- DESIGN.md: full design incl. driving-trip requirements (R1-R4), stays model, focus mode, mobile, provenance rules - SURVEY.md: open-source landscape - mock/: interaction mock (Florence itinerary, focus mode, stays, region stops, mobile layout) - router/: Go module (stdlib-only) with Router interface (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor, routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing), integration tests, and setup-osrm.sh for the self-hosted router - osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
115 lines
4.9 KiB
Markdown
115 lines
4.9 KiB
Markdown
# maps/router — route-aware planning primitives
|
||
|
||
The Go backend for the "search along the route" capability
|
||
([DESIGN.md](../DESIGN.md), "Driving trips" section). Everything the LLM
|
||
would otherwise *guess* about a trip — route shape, travel times, detour
|
||
costs — is computed here and returned with provenance.
|
||
|
||
## What's in here
|
||
|
||
```
|
||
internal/geo/ dependency-free geodesy (haversine, point-to-line)
|
||
internal/route/ Router interface + backends
|
||
- valhalla.go: hosted/self-hosted Valhalla (works today;
|
||
no geometry on the hosted instance → chord fallback)
|
||
- osrm.go: local OSRM (full geometry; scripts/setup-osrm.sh)
|
||
internal/plan/ the primitives:
|
||
- stopcost.go: stop_cost = route(A,C via B) − route(A,C)
|
||
- optimize.go: optimize_stops (exhaustive ≤10 stops, greedy above)
|
||
- corridor.go: corridor = buffer band around the route
|
||
cmd/routectl/ manual CLI: route | stopcost | optimize
|
||
cmd/bench/ benchmark runner (bench/tasks.json)
|
||
bench/tasks.json 5 real NE-corridor tasks with recorded goldens
|
||
scripts/setup-osrm.sh self-host OSRM over the NH+MA+CT+NY extract
|
||
```
|
||
|
||
Zero third-party Go dependencies (stdlib only).
|
||
|
||
## Try it (hosted Valhalla, no setup)
|
||
|
||
```sh
|
||
go run ./cmd/routectl route --from 44.054,-71.650 --to 40.729,-73.966
|
||
# Lincoln NH → Queens NY: ~5h32m, 531 km, I-93/90/495/290 (NOT via Boston)
|
||
|
||
go run ./cmd/routectl stopcost --from 44.054,-71.650 --to 40.729,-73.966 \
|
||
--stop 42.2767,-71.806:60:cascade \
|
||
--stop 43.507,-71.548:50:ladd \
|
||
--stop 43.642,-71.156:120:mtmajor
|
||
# cascade detour= 6 min (on the I-495 corridor)
|
||
# ladd detour=17 min
|
||
# mtmajor detour=80 min (off-corridor)
|
||
|
||
go run ./cmd/routectl optimize --from 44.054,-71.650 --to 40.729,-73.966 --k 2 \
|
||
--stop 42.2767,-71.806:60:cascade --stop 43.507,-71.548:50:ladd \
|
||
--stop 43.902,-71.64:60:plymouth --stop 42.425,-71.68:60:brewsters
|
||
# order: ladd -> cascade detour: 23 min
|
||
```
|
||
|
||
## Tests
|
||
|
||
```sh
|
||
go test ./... # unit tests, offline, fast
|
||
go test -tags integration ./... # live router (Valhalla by default)
|
||
ROUTER_BACKEND=osrm ROUTER_URL=http://localhost:5000 \
|
||
go test -tags integration ./... # against local OSRM
|
||
```
|
||
|
||
The integration tests encode the key regression: the fastest
|
||
Lincoln→Queens drive stays ≥15 km from Boston (it goes via Worcester,
|
||
I-90/I-495/I-290). That is exactly the fact an LLM confidently gets wrong.
|
||
|
||
## Benchmark
|
||
|
||
```sh
|
||
go run ./cmd/bench # run all tasks, assert goldens + corridor checks
|
||
go run ./cmd/bench --record # re-record goldens (after a backend/extract change)
|
||
```
|
||
|
||
Tasks are real OD pairs across NH/MA/CT/NY with real POI candidates and
|
||
dwell times. Check types:
|
||
|
||
| type | meaning |
|
||
|---|---|
|
||
| `golden_route_min` | direct route time within a recorded band (±10%) |
|
||
| `route_avoids` | route stays ≥ N km from a point (geometry, or bbox when the backend has none) |
|
||
| `stop_free` / `stop_detour_at_least` / `stop_detour_band` | computed detour of a candidate vs an expected band |
|
||
| `optimize_order` | the exhaustive optimum matches the recorded order |
|
||
| `crosscheck` | single multi-waypoint route ≈ matrix-summed prediction (≤ maxPct drift) |
|
||
|
||
Current state: **26 PASS / 0 FAIL** against hosted Valhalla.
|
||
|
||
## Self-hosted OSRM (full geometry, exact corridors)
|
||
|
||
`scripts/setup-osrm.sh` builds OSRM 26.4.1 from source (Boost header-only),
|
||
extracts the 4-state PBFs (pre-downloaded in `../osm/`), partitions,
|
||
customizes, and serves on `:5000`. Then:
|
||
|
||
```sh
|
||
go run ./cmd/routectl route --backend osrm --from 44.054,-71.650 --to 40.729,-73.966
|
||
```
|
||
|
||
With OSRM, `Route.Geometry` is populated, so the corridor is exact
|
||
(distances to the actual routed polyline) instead of the chord fallback
|
||
used with the hosted Valhalla. This is what makes
|
||
`search_along_route`'s spatial pre-filter (PostGIS `ST_DWithin` on the
|
||
corridor polygon in the v1 design) sound.
|
||
|
||
## Design notes
|
||
|
||
- **The route is computed, never narrated.** No function in this module
|
||
returns a time estimate from model knowledge; every number is router
|
||
output (`computed` provenance per DESIGN.md R4).
|
||
- **Matrix via pairwise route calls.** Valhalla's
|
||
`sources_to_targets` caps at 150 km, so the common path is
|
||
`BuildMatrix` (pairwise `/route`). OSRM's `Table` service is available
|
||
for the local backend when a single-call matrix matters.
|
||
- **optimize_stops semantics**: the user asked for *k* stops ("two
|
||
hikes"), so the objective minimizes total = detour + dwell + overhead
|
||
over ordered subsets of **exactly k**, with fallback (a) to the best
|
||
feasible smaller set when a time budget is given, then (b) to the best
|
||
exactly-k ignoring the budget, flagged `Feasible=false`.
|
||
"At most k" without a budget degenerates (zero stops always wins).
|
||
- **Stop coordinates are POI centroids.** The router snaps to the
|
||
network; a 200 m centroid error moves a 531 km route's detour by
|
||
seconds, not minutes.
|