/* * polyline.ts — Google encoded-polyline codec at precision 1e6 ("polyline6"). * ----------------------------------------------------------------------------- * This is the encoding the OSRM demo's `geometries=polyline6` returns and what * the fixture corpus stores VERBATIM (RESEARCH Finding 1): the committed string * is exactly what the server returned — no float rounding, no re-serialization. * * Algorithm (standard Google encoded-polyline, lat then lng per point): * encode value = round(coordinate * 1e6); delta = value - previous value * (previous seeded at 0); zigzag the delta (negative v -> -v*2 - 1, * else v*2); emit 5-bit chunks little-end first with continuation * bit 0x20 on every chunk but the last; each chunk + 63 is the * output character. * decode the exact inverse; the accumulated integer is divided by 1e6. * * Because every decoded value lives on the 1e-6 grid, the ROUND-TRIP LAW * holds: encodePolyline6(decodePolyline6(s)) === s for every valid string — * the fixture corpus gate (plan 05-02) leans on this as a free per-pair * assertion. * * T-05-02: malformed input — a coordinate cut off mid-varint (dangling * continuation chunk, odd trailing coordinate) — THROWS "truncated polyline6 * input". The decoder never returns partial or NaN points that could poison a * map draw. "" decodes to [] (a legal empty geometry). * * The point shape is declared structurally (identical to types.ts RoutePoint) * because this module is PURE: zero imports, zero I/O — the * search/src/distance.ts precedent (a dependency would weight the Worker and * browser bundles for zero gain). Arithmetic uses double math (values stay far * below 2^53), not int32 bit shifts, so no coordinate range can silently wrap. */ /** Minimal structural point — structurally identical to types.ts RoutePoint. */ interface PolyPoint { lat: number; lng: number; } /** Encode one zigzagged delta as a 5-bit-chunk varint over the +63 alphabet. */ function encodeVarint(value: number): string { let v = value < 0 ? -value * 2 - 1 : value * 2; let out = ""; while (v >= 0x20) { out += String.fromCharCode((v % 32) + 0x20 + 63); v = Math.floor(v / 32); } return out + String.fromCharCode(v + 63); } /** Decode polyline6 exactly; throws on truncated input (never partial points). */ export function decodePolyline6(encoded: string): PolyPoint[] { const points: PolyPoint[] = []; const n = encoded.length; let index = 0; let lat = 0; let lng = 0; while (index < n) { // lat then lng per point — a coordinate cut off mid-varint throws (T-05-02). for (let axis = 0; axis < 2; axis++) { let result = 0; let factor = 1; let continuation = true; while (continuation) { if (index >= n) throw new Error("truncated polyline6 input"); const code = encoded.charCodeAt(index) - 63; index += 1; result += (code & 0x1f) * factor; factor *= 32; continuation = (code & 0x20) !== 0; } const delta = result % 2 === 1 ? -((result + 1) / 2) : result / 2; if (axis === 0) { lat += delta; } else { lng += delta; } } points.push({ lat: lat / 1e6, lng: lng / 1e6 }); } return points; } /** Encode points to polyline6 (deltas accumulate left to right; 1e-6 grid). */ export function encodePolyline6(points: readonly PolyPoint[]): string { let out = ""; let prevLat = 0; let prevLng = 0; for (const point of points) { const lat = Math.round(point.lat * 1e6); const lng = Math.round(point.lng * 1e6); out += encodeVarint(lat - prevLat); out += encodeVarint(lng - prevLng); prevLat = lat; prevLng = lng; } return out; }