Problem
src/routing.ts is hardcoded to FOSSGIS Valhalla at valhalla1.openstreetmap.de. When FOSSGIS takes that service offline (currently down for several days at the time of writing — both valhalla1 and valhalla2 refuse TCP on :80 and :443 while the hosts themselves remain pingable), the app loses all turn-by-turn navigation with no recourse. The only thing the user sees is the routing-unavailable dialog from #188.
We have no swappable routing provider. Adding OSRM at routing.openstreetmap.de (a stable peer service hosted by OSM-DE, currently up while FOSSGIS Valhalla is down) requires first refactoring routing.ts into a provider-agnostic interface, then implementing an OSRM client alongside the existing Valhalla one.
API delta between Valhalla and OSRM
| Concern |
Valhalla (current) |
OSRM (new) |
| Endpoint |
POST /route + JSON body |
GET /route/v1/{profile}/{lon},{lat};{lon},{lat}?overview=full&geometries=polyline6&steps=true |
| Profile values |
auto, bicycle, pedestrian |
driving, cycling, walking (1:1 map) |
| Geometry |
polyline6 (encoded in leg.shape) |
polyline6 (when requested) — decodePolyline6() is shared |
| Step distance/duration |
length (in unit) + time (s) — needs metersPerValhallaUnit() factor |
distance (m) + duration (s) directly |
| Instruction text |
Prose string from Valhalla's narrative generator (step.instruction) |
Not provided — must be synthesized from maneuver.type + maneuver.modifier + step.name |
| Units option |
directions_options.units (km/mi) |
Always metric internally — our units.ts formats for display, so this is a no-op for us |
| Costing config |
costing field |
Profile is in the URL path |
Expected
fetchRoute() keeps its external signature; callers in guidance.ts are unchanged.
- Provider selected by a const (e.g.
ROUTING_PROVIDER: 'valhalla' | 'osrm' = 'valhalla'). Runtime fallback is a separate issue.
- Both providers produce the same
Route shape: coords, steps[] with instruction / type / lengthM / durationS / streetNames / beginShapeIndex, plus distanceM and durationS.
- OSRM
maneuver.type values (turn, new name, depart, arrive, merge, on ramp, off ramp, fork, end of road, continue, roundabout, rotary, roundabout turn, notification) are mapped to either our existing numeric Valhalla maneuver-type or a parallel string union — decide during implementation. Whichever direction, the icon-selection in guidance.ts must keep working.
- Instruction synthesis: small lookup table combining
type + modifier + name into English (e.g. "Turn right onto Main St", "Continue on Highway 5", "Arrive at destination"). Localization is out of scope.
- Both providers covered in
src/routing.test.ts with response fixtures.
Scope
src/routing.ts — extract provider-specific request building and response parsing; introduce a RoutingProvider interface (or two free functions behind a switch).
src/routing-osrm-instructions.ts (new) — instruction synthesis from maneuver.type + modifier + step.name.
src/routing.test.ts — parallel fixtures and tests for OSRM responses.
- No UI changes.
guidance.ts, the pill, and the costing chip keep using the same Route shape.
Out of scope
- Automatic failover / health detection (separate follow-up issue).
- Switching the default provider — one-line change after this lands.
- Self-hosted Valhalla.
- Instruction localization.
Problem
src/routing.tsis hardcoded to FOSSGIS Valhalla atvalhalla1.openstreetmap.de. When FOSSGIS takes that service offline (currently down for several days at the time of writing — bothvalhalla1andvalhalla2refuse TCP on :80 and :443 while the hosts themselves remain pingable), the app loses all turn-by-turn navigation with no recourse. The only thing the user sees is the routing-unavailable dialog from #188.We have no swappable routing provider. Adding OSRM at
routing.openstreetmap.de(a stable peer service hosted by OSM-DE, currently up while FOSSGIS Valhalla is down) requires first refactoringrouting.tsinto a provider-agnostic interface, then implementing an OSRM client alongside the existing Valhalla one.API delta between Valhalla and OSRM
POST /route+ JSON bodyGET /route/v1/{profile}/{lon},{lat};{lon},{lat}?overview=full&geometries=polyline6&steps=trueauto,bicycle,pedestriandriving,cycling,walking(1:1 map)polyline6(encoded inleg.shape)polyline6(when requested) —decodePolyline6()is sharedlength(in unit) +time(s) — needsmetersPerValhallaUnit()factordistance(m) +duration(s) directlystep.instruction)maneuver.type+maneuver.modifier+step.namedirections_options.units(km/mi)units.tsformats for display, so this is a no-op for uscostingfieldExpected
fetchRoute()keeps its external signature; callers inguidance.tsare unchanged.ROUTING_PROVIDER: 'valhalla' | 'osrm' = 'valhalla'). Runtime fallback is a separate issue.Routeshape:coords,steps[]withinstruction/type/lengthM/durationS/streetNames/beginShapeIndex, plusdistanceManddurationS.maneuver.typevalues (turn,new name,depart,arrive,merge,on ramp,off ramp,fork,end of road,continue,roundabout,rotary,roundabout turn,notification) are mapped to either our existing numeric Valhalla maneuver-type or a parallel string union — decide during implementation. Whichever direction, the icon-selection inguidance.tsmust keep working.type+modifier+nameinto English (e.g."Turn right onto Main St","Continue on Highway 5","Arrive at destination"). Localization is out of scope.src/routing.test.tswith response fixtures.Scope
src/routing.ts— extract provider-specific request building and response parsing; introduce aRoutingProviderinterface (or two free functions behind a switch).src/routing-osrm-instructions.ts(new) — instruction synthesis frommaneuver.type+modifier+step.name.src/routing.test.ts— parallel fixtures and tests for OSRM responses.guidance.ts, the pill, and the costing chip keep using the sameRouteshape.Out of scope