You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#2571 made a catchment-routed submission always carry a coordinate: if the applicant never picked a geocode suggestion, the server fills the coordinate from the parish they selected. That fixed the real bug — the CMS was receiving applications with no location at all — but the filled value is an invention, and the shape of the invention has already had to change once.
The points started as the serving polyclinic's own location, which put the CMS map pin on the clinic building. 77bbb0ca moved them to parish centres, which reads honestly ("somewhere in this parish, location not captured"). But a parish centre still has to sit inside the catchment the parish routes to, and catchment boundaries do not follow parish lines — so two of the eleven had to be nudged off their true centroid, and they landed effectively on the boundary. Measured against polyclinic-catchments.geojson:
Parish
Margin to its catchment boundary
st-peter / st-lucy / st-andrew
2.5–3.1 km
st-philip / st-thomas / st-joseph / st-john
1.4–2.8 km
st-james
823 m
st-michael
58 m
st-george
7 m
christ-church
2 m
Containment is unambiguous today (exactly one catchment contains each point, so pointHit's first-match-wins is never deciding by feature order), and parish-routing-point.spec.ts asserts the per-parish invariant, so a regression fails in CI rather than misrouting silently. But any correction, re-export or simplification of the catchment GeoJSON moves boundaries by far more than 7 m, and Christ Church and St George would flip.
Re-deriving those three for maximum margin would buy time. It would not change the fact that we are guessing at a location the applicant could simply have told us.
Proposed resolution
Stop guessing. Make the coordinate something the applicant supplies, always, by one of two routes:
Pick a geocode suggestion — unchanged, and still the fast path for most applicants.
Place the pin themselves — when none of the suggestions is right, the address field offers to open a map in a modal. The applicant drags a pin to the premises and confirms. The modal also offers use my current location (Geolocation API) as a shortcut for someone standing at the premises.
With that in place:
Free-text addresses are no longer accepted for a catchment-routed form. Today typing an address that never geocodes is a silent dead end for routing; it would become a field the applicant cannot leave until they have either picked a suggestion or placed a pin.
The parish fallback goes away entirely.PARISH_ROUTING_POINTS, fillParishRoutingCoordinate and its spec all delete. So does parishHit / the routing use of PARISH_DEFAULTS in CatchmentRoutingService, and eventually catchmentRouting.parishField in the recipe schema (a migration across the 12 routed recipes — worth phasing separately from the UI work).
Several conditions in catchment-coordinate-to-cams.spec.ts collapse: "typed it, never picked", "left a blank coordinate", "left a malformed coordinate" all become unreachable rather than fallback cases.
Things to settle before building
These are the questions that would change the design, not a checklist to work through afterwards.
Accessibility is the hard one. Dragging a pin on a map is not operable by keyboard or screen reader, and Standard 5 / WCAG 2.1 AA apply. "Free text is no longer allowed" and "the map is the only alternative to a suggestion" cannot both hold without excluding people. Options worth testing: keyboard-operable pin nudging with an announced address readback; a "none of these is right" path that captures a written description plus the parish and flags the case for an Environmental Health Officer to resolve; or accepting a coarser coordinate from a keyboard-only path and marking its provenance. Needs a decision with content and research, not a technical fix.
Provenance, not just format.isRoutingCoordinate is a format check — nothing downstream can tell a geocoded coordinate from a dropped pin from an invented centroid. If a case reviewer is going to act on the pin, the payload should say where it came from (geocoded / placed / device). That is the thing that actually fixes "a pin that looks precise but isn't", and it is worth doing whether or not the map ships.
Geolocation can reproduce the exact bug we are fixing. A citizen or an agent filling the form at home, or at an office, would drop a confidently precise pin in the wrong place. Device accuracy should be read and shown, the value should be labelled as device-derived, and the applicant should confirm it on the map rather than have it accepted silently.
Map tiles are new infrastructure. The monorepo has no map library today, and the only mapping dependency is the Nominatim proxy at /geocode. Tiles mean a provider (self-hosted, or a service with its CSP origin allowed), a bundle-size decision, and a story for what the field does when tiles fail to load — which must not be "the applicant cannot submit".
Privacy. For the forms where the routed address is someone's home (the hairdresser licence routes on personal-details.address-coordinates), we would be capturing a precise home coordinate where we previously captured a parish. Worth a look from the privacy notice angle.
Existing data. Submissions already carry parish-centroid coordinates. Anything that later reads coordinates as precise needs to know those are not.
Suggested phasing
Provenance on the coordinate (small, independently useful, unblocks the rest).
Accessibility decision on the non-map path — before any UI is built.
Map modal: pin placement + geolocation, with the suggestion path unchanged.
Turn off free text for routed forms; delete the parish fallback and its tests.
Recipe migration to drop catchmentRouting.parishField.
Steps 4 and 5 are the ones that remove code; nothing before them changes routing behaviour, so each can ship on its own.
Context: #2571 (the fallback this replaces), #2573 (surfaces the 422 the hard requirement depends on), #2574 (the payment-gated path, unrelated but touches the same resolution).
Why the current fallback is a fudge
#2571 made a catchment-routed submission always carry a coordinate: if the applicant never picked a geocode suggestion, the server fills the coordinate from the parish they selected. That fixed the real bug — the CMS was receiving applications with no location at all — but the filled value is an invention, and the shape of the invention has already had to change once.
The points started as the serving polyclinic's own location, which put the CMS map pin on the clinic building.
77bbb0camoved them to parish centres, which reads honestly ("somewhere in this parish, location not captured"). But a parish centre still has to sit inside the catchment the parish routes to, and catchment boundaries do not follow parish lines — so two of the eleven had to be nudged off their true centroid, and they landed effectively on the boundary. Measured againstpolyclinic-catchments.geojson:Containment is unambiguous today (exactly one catchment contains each point, so
pointHit's first-match-wins is never deciding by feature order), andparish-routing-point.spec.tsasserts the per-parish invariant, so a regression fails in CI rather than misrouting silently. But any correction, re-export or simplification of the catchment GeoJSON moves boundaries by far more than 7 m, and Christ Church and St George would flip.Re-deriving those three for maximum margin would buy time. It would not change the fact that we are guessing at a location the applicant could simply have told us.
Proposed resolution
Stop guessing. Make the coordinate something the applicant supplies, always, by one of two routes:
With that in place:
PARISH_ROUTING_POINTS,fillParishRoutingCoordinateand its spec all delete. So doesparishHit/ the routing use ofPARISH_DEFAULTSinCatchmentRoutingService, and eventuallycatchmentRouting.parishFieldin the recipe schema (a migration across the 12 routed recipes — worth phasing separately from the UI work).SubmissionsService.submitstops being a backstop and becomes the real contract. fix(forms): surface server validation errors on the fields they name #2573 already surfaces its 422 on the field it names, so the failure mode is actionable.catchment-coordinate-to-cams.spec.tscollapse: "typed it, never picked", "left a blank coordinate", "left a malformed coordinate" all become unreachable rather than fallback cases.Things to settle before building
These are the questions that would change the design, not a checklist to work through afterwards.
Accessibility is the hard one. Dragging a pin on a map is not operable by keyboard or screen reader, and Standard 5 / WCAG 2.1 AA apply. "Free text is no longer allowed" and "the map is the only alternative to a suggestion" cannot both hold without excluding people. Options worth testing: keyboard-operable pin nudging with an announced address readback; a "none of these is right" path that captures a written description plus the parish and flags the case for an Environmental Health Officer to resolve; or accepting a coarser coordinate from a keyboard-only path and marking its provenance. Needs a decision with content and research, not a technical fix.
Provenance, not just format.
isRoutingCoordinateis a format check — nothing downstream can tell a geocoded coordinate from a dropped pin from an invented centroid. If a case reviewer is going to act on the pin, the payload should say where it came from (geocoded/placed/device). That is the thing that actually fixes "a pin that looks precise but isn't", and it is worth doing whether or not the map ships.Geolocation can reproduce the exact bug we are fixing. A citizen or an agent filling the form at home, or at an office, would drop a confidently precise pin in the wrong place. Device accuracy should be read and shown, the value should be labelled as device-derived, and the applicant should confirm it on the map rather than have it accepted silently.
Map tiles are new infrastructure. The monorepo has no map library today, and the only mapping dependency is the Nominatim proxy at
/geocode. Tiles mean a provider (self-hosted, or a service with its CSP origin allowed), a bundle-size decision, and a story for what the field does when tiles fail to load — which must not be "the applicant cannot submit".Privacy. For the forms where the routed address is someone's home (the hairdresser licence routes on
personal-details.address-coordinates), we would be capturing a precise home coordinate where we previously captured a parish. Worth a look from the privacy notice angle.Existing data. Submissions already carry parish-centroid coordinates. Anything that later reads coordinates as precise needs to know those are not.
Suggested phasing
catchmentRouting.parishField.Steps 4 and 5 are the ones that remove code; nothing before them changes routing behaviour, so each can ship on its own.
Context: #2571 (the fallback this replaces), #2573 (surfaces the 422 the hard requirement depends on), #2574 (the payment-gated path, unrelated but touches the same resolution).