# Phase 5: Routing + Route Display — Research (light pass) **Researched:** 2026-10-24 (light pass — the pivot removed Valhalla; per roadmap note "standard patterns, no research phase needed", this pass covers only the three open questions planning actually faces) **Scope:** fixture encoding format · OSRM demo build-time script shape · RoutingSource interface analogs. Everything else is decided in 05-CONTEXT.md (D-01..D-12) and 05-UI-SPEC.md (binding). --- ## Finding 1 — Fixture encoding: encoded polyline6 VERBATIM (not GeoJSON) **Decision:** store each route as the encoded-polyline string exactly as the OSRM demo returns it (`geometries=polyline6`), plus `distanceM`/`durationS`. A decoder lives in the routing workspace. - **Verified live (2026-10-24):** `GET https://router.project-osrm.org/route/v1/driving/101.7117,3.1578;101.6869,3.1343?alternatives=2&overview=full&geometries=polyline6` → `code: Ok`, 2 routes, route 1 = 6907.9 m / 663.2 s with a **1200-char** geometry string; route 2 = 6572.8 m / 696 s / 1252 chars. - **Size math vs the D-11 budget:** ~45 routes (15 pairs × up to 3) × ~1.2–2.5 KB ≈ **40–60 KB raw → ~12–20 KB gzipped**. The ≤ 200 KB gz budget leaves an order of magnitude of headroom. GeoJSON coordinate arrays for the same geometry measure ~10–20× larger for zero fidelity gain — rejected. - **Verbatim storage is the lossless choice:** the string is what the demo server returned — no float rounding, no re-serialization. The decoder (standard Google encoded-polyline varint/delta algorithm, precision 1e6) is ~40 lines, pure, and round-trip testable (encode(decode(s)) === s per fixture — a free corpus assertion). - **Snapped-progress support (D-07, GUID-05/06 upstream obligation):** fixtures do NOT precompute cumulative distances — the runtime derives `cumulativeDistanceM` once at decode/load (deterministic haversine walk, memoized). Keeps fixtures lean; the MODEL still carries the field from day one. ## Finding 2 — OSRM demo build-time script shape (verified shape, repo-tool precedent) - **Endpoint:** `https://router.project-osrm.org/route/v1/driving/{lng},{lat};{lng},{lat}?alternatives=2&overview=full&geometries=polyline6` — `alternatives=2` returns up to 2 alternates (≤ 3 total routes; matches RTNG-02's "up to 3"), `overview=full` gives the complete geometry, `steps=false` (turn instructions are Phase 6; fixture `steps` stay typed-but-empty — never fake). - **Response contract:** `code: "Ok"`, `routes[]` best-first (preserve order), each with `distance` (m), `duration` (s), `geometry` (polyline6 string). A `code !== "Ok"` or 0-route response is a script HARD FAILURE for that pair — never a silently missing pair. - **Script posture — exact analog `search/scripts/overpass-topup.mjs` (Phase 4 D-06 "repo tools never runtime deps"):** runs on the MAINTAINER's machine (`npm run routing:fixtures`), never in CI, never at `vite build`, never at Worker request time. Mandatory descriptive User-Agent, polite delay between pair fetches, writes the committed `routing/src/data/route-fixtures.json`, soft-fails with a per-pair diagnostic. **D-12 boundary stated in the script header:** it hits FIXED PUBLIC coordinates (gazetteer landmark points) only; no user coordinate can ever enter a fixture; the app itself gains no network dependency. - **Sustainability honesty:** the demo server is a free best-effort service. That is acceptable precisely because its output is COMMITTED — the runtime never touches it. A server policy change breaks the (rare) regeneration script, never the product. ## Finding 3 — RoutingSource interface (the SearchSource analog) Mirror of `search/src/types.ts` (verbatim shape correspondence, D-01): | SearchSource (Phase 4, shipped) | RoutingSource (this phase) | |---|---| | `readonly name: string` | `readonly name: string` — `"fixture"` now; `"wasm-router"` (#2) / `"valhalla"` (#3) documented, not shipped | | `search(q): Promise` | `route(req): Promise` | | EMPTY array = honest refusal (D-13) | **`null` = honest "router pending" (D-03)** — never a blank map, never a fake route | | `byId(id): GazetteerEntry \| null` (sync) | not needed this phase (the webview already holds the resolved `GazetteerEntry` from Phase 4) | - `RouteResult` carries `options: RouteOption[]` — **index 0 is the best route** (OSRM returns best-first; order preserved), 1..3 entries. Each option: geometry (decoded points + derived cumulative distances), summary (distance/duration), and the day-one `steps` field (typed, empty in fixture world — D-10 defers instructions to Phase 6; an empty array is honest "not wired", a fabricated step list would not be). - **Fixture matching law:** a pair serves a request when `destinationId` matches exactly AND the requested origin sits within a fixed public tolerance (200 m haversine) of the pair's pinned origin. Anything else → `null` (the pending state). The 200 m pad absorbs GPS jitter around a known landmark without ever fabricating a route for an unknown origin. - **Lazy-index + three-environment laws carry over verbatim** from `search/src/gazetteer.ts` (LAW 1 lazy memoized load; LAW 2 static JSON import inlined by esbuild AND Vite, importable by bare Node 24 with `with { type: "json" }`; LAW 3 no logging/no storage — the D-15 analog: request coordinates never leave the caller). ## Validation Architecture - **Framework:** existing hand-rolled runners; plan 05-01 adds the fifth workspace's identical pair (`routing/test/runner.mjs` + `routing/test/harness.mjs`, copied verbatim from `search/test/`). No new framework (Phase 4 precedent). - **Primary acceptance instruments:** (1) the routing workspace suite (codec vectors, contract gates, lazy/no-logging/no-storage laws); (2) the golden-pair corpus gate (offline: every pair's geometry↔summary consistency recomputed by haversine + round-trip codec proof + alternatives floor) — the D-14 corpus-gate shape, adapted to fixtures; (3) the web suite extensions (route state machine, copy gate, BackButton walk); (4) BA9 bundle-budget gate over the production build; (5) the capture harness legs (`?fixpair=`) with the `ARAHARAH-ROUTE state=` token. - **Offline law:** no test, suite, or gate ever touches a network. The OSRM demo is exercised ONLY by the maintainer script. This is what makes the corpus gate deterministic and the "fixture resolves cannot fail at runtime" claim (UI-SPEC Copywriting §Error state) true. - **Blocking checkpoint:** none this phase — no real-device leg is required (route display is fixture-world; the Phase 3/4 device gates already proved map + deep-link on hardware). Capture legs + structural gates carry verification. --- ## RESEARCH COMPLETE *Light pass as directed: three findings, folded into plans 05-01/05-02/05-04/05-05. No open questions remain.*