diff --git a/routing/scripts/measure-optimality-baseline.mjs b/routing/scripts/measure-optimality-baseline.mjs new file mode 100644 index 0000000..e7a57ac --- /dev/null +++ b/routing/scripts/measure-optimality-baseline.mjs @@ -0,0 +1,217 @@ +#!/usr/bin/env node +/* + * measure-optimality-baseline.mjs — the pre-ALT optimality baseline recorder + * (Phase 8 plan 08-01; D-03's comparison anchor). + * ------------------------------------------------------------------------------ + * A MAINTAINER-TIME REPO TOOL — NOT a runtime dependency; nothing in bot/src, + * web/src, or routing/src imports it. It records the PRE-ALT route seconds for + * 100 coordinate-pinned cross-metro pairs into a COMMITTED JSON record — the + * anchor the D-03 optimality gate (Plan 08-05's measure-optimality-gate.mjs) + * compares against after the ALT preprocessing lands. + * + * RUN THIS BEFORE THE ARG2 CODEC LANDS (research Pitfall 1): once graph.ts + * versions to 2, decodeGraph rejects the v1 binary and "old code + old binary" + * is unreproducible from the working tree. The recorder enforces this + * mechanically — it reads the binary's version field and REFUSES anything + * that is not ARG1, so a post-codec run can never silently fake a pre-ALT + * baseline. + * + * THE RECORD IS IMMUTABLE: the script refuses to overwrite an existing record + * ("delete by explicit git revert only"). Pairs are pinned by COORDINATES + * (lat/lng drawn from the graph's order-independent bbox by a seeded PRNG) — + * never node indices (Pitfall 2: a rebuild reorders nodes; coordinates do not + * move). Route seconds are stored as JSON numbers — routeAstar returns exact + * f64 accumulations of f32 edgeSeconds and JSON round-trips IEEE doubles + * losslessly (research A8). + * + * Echo law: pair ids + verdict classes + seconds ONLY — coordinates in the + * record are public bbox-derived values, never user data (the corpus's D-12 + * posture, applied to a maintainer tool). + * + * Usage: + * node routing/scripts/measure-optimality-baseline.mjs + * + * Exit codes: 0 recorded; 1 already recorded, graph absent/not ARG1, or + * decode failure. A run that measures but fails a gate writes NOTHING. + */ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts +const GRAPH_PATH = resolve(here, "..", "data", "kl-roadgraph.v1.bin"); +const RECORD_PATH = resolve(here, "..", "test", "fixtures", "alt-baseline.json"); + +/** D-04's sample size for the optimality record (~100 cross-metro pairs). */ +const BASELINE_PAIR_COUNT = 100; +/** Shared pair-set seed — the SAME seed measure-route-cpu.mjs --extended + * reuses, so the baseline and the CPU verdict measure identical pairs. */ +const BASELINE_SEED = 0x08a1; +/** Cross-metro floor: straight-line endpoint distance below this is NOT a + * worst-case pair (the 20–30 km class is where the budget breaks — §1.9). */ +const MIN_PAIR_KM = 12; +/** The binary version the baseline MUST be recorded on (Pitfall 1's guard). */ +const REQUIRED_ARG_VERSION = 1; +/** The epsilon the D-03 gate will assert against (documentary here; the gate + * mirrors this value — a record edit alone cannot fake equality). */ +const OPTIMALITY_EPSILON_PCT = 0.1; +/** Honest-termination guard for the rejection sampler. */ +const MAX_SAMPLER_ATTEMPTS = 50_000; + +function fail(message) { + console.error(`measure-optimality-baseline: ${message}`); + process.exit(1); +} + +function toArrayBuffer(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +/** mulberry32 — tiny deterministic PRNG (fixed seed => comparable runs). + * Copied verbatim from measure-route-cpu.mjs (one PRNG shape in the repo). */ +function mulberry32(seed) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Exact-decimal rounding to 1e-6 (the codec's coordinate grid law). */ +const round6 = (v) => Math.round(v * 1e6) / 1e6; + +/** + * The pair generator — exported so the extended CPU sample (Plan 08-01 task 3) + * measures the SAME pairs from ONE definition (no drift between reporters). + * Deterministic: same bbox + seed ⇒ identical pairs, always. + * + * @param bbox {{ minLat: number, minLng: number, maxLat: number, maxLng: number }} + * @param opts {{ count: number, minPairKm: number, seed: number, haversineM: (a, b) => number }} + * @returns Array<{ pairId, origin: {lat, lng}, destination: {lat, lng}, straightLineKm: number }> + */ +export function generateCrossMetroPairs(bbox, opts) { + const { count, minPairKm, seed, haversineM } = opts; + const rand = mulberry32(seed); + const draw = (axis) => + axis.min + rand() * (axis.max - axis.min); + const pairs = []; + let attempts = 0; + while (pairs.length < count) { + if (++attempts > MAX_SAMPLER_ATTEMPTS) { + fail(`sampler gave up after ${MAX_SAMPLER_ATTEMPTS} attempts — bbox too small for the ${minPairKm} km floor`); + } + const origin = { lat: round6(draw({ min: bbox.minLat, max: bbox.maxLat })), lng: round6(draw({ min: bbox.minLng, max: bbox.maxLng })) }; + const destination = { lat: round6(draw({ min: bbox.minLat, max: bbox.maxLat })), lng: round6(draw({ min: bbox.minLng, max: bbox.maxLng })) }; + const straightLineKm = haversineM(origin, destination) / 1000; + if (straightLineKm < minPairKm) continue; // not a cross-metro worst case + pairs.push({ + pairId: `x${String(pairs.length).padStart(3, "0")}`, + origin, + destination, + straightLineKm: Math.round(straightLineKm * 1000) / 1000, + }); + } + return pairs; +} + +/** + * The measurement core — exported for in-process mirrors of the same numbers. + * Honest nulls are MEASUREMENTS: a snap miss records verdict "unsnappable", + * a disconnected pair records "unreachable" — neither is skipped silently. + * + * @returns Array<{ pairId, origin, destination, straightLineKm, seconds: number|null, verdict: "reachable"|"unreachable"|"unsnappable" }> + */ +export function measureBaselinePairs(graph, pairs, opts) { + const { snap, astar } = opts; + return pairs.map((pair) => { + const originSnap = snap(graph, pair.origin); + const goalSnap = snap(graph, pair.destination); + if (originSnap === null || goalSnap === null) { + return { ...pair, seconds: null, verdict: "unsnappable" }; + } + const result = astar(graph, originSnap.node, goalSnap.node); + if (result === null) { + return { ...pair, seconds: null, verdict: "unreachable" }; + } + return { ...pair, seconds: result.seconds, verdict: "reachable" }; + }); +} + +async function main() { + if (!existsSync(GRAPH_PATH)) { + fail(`no graph at ${GRAPH_PATH} — build it first: node routing/scripts/build-road-graph.mjs`); + } + if (existsSync(RECORD_PATH)) { + fail(`baseline already recorded at ${RECORD_PATH} — the record is immutable; delete by explicit git revert only`); + } + + const buffer = readFileSync(GRAPH_PATH); + if (buffer.byteLength < 64) fail(`${GRAPH_PATH} is not a road-graph binary`); + const version = new DataView(toArrayBuffer(buffer)).getUint32(4, true); + if (version !== REQUIRED_ARG_VERSION) { + fail(`the on-disk binary is ARG${version} — the baseline MUST be recorded on the ARG1 binary, BEFORE the ARG2 codec lands (Pitfall 1)`); + } + + const { decodeGraph } = await import("../src/graph.ts"); + const { snapToGraph } = await import("../src/snap.ts"); + const { routeAstar } = await import("../src/astar.ts"); + const { haversineM } = await import("../src/geo.ts"); + + const graph = decodeGraph(toArrayBuffer(buffer)); + if (graph === null) fail(`${GRAPH_PATH} does not decode (fail-honest null) — rebuild the binary`); + + const pairs = generateCrossMetroPairs(graph.bbox, { + count: BASELINE_PAIR_COUNT, + minPairKm: MIN_PAIR_KM, + seed: BASELINE_SEED, + haversineM, + }); + const rows = measureBaselinePairs(graph, pairs, { snap: snapToGraph, astar: routeAstar }); + + // Print the measurement (every pair, then the summary) — print-the-measurement law. + for (const row of rows) { + const value = row.seconds === null ? row.verdict : `${row.seconds.toFixed(1)}s ${row.verdict}`; + console.log(` ${row.pairId}: ${value}`); + } + const routed = rows.filter((r) => r.verdict === "reachable"); + const unreachable = rows.filter((r) => r.verdict === "unreachable"); + const unsnappable = rows.filter((r) => r.verdict === "unsnappable"); + const seconds = routed.map((r) => r.seconds); + const mean = seconds.reduce((acc, v) => acc + v, 0) / (seconds.length || 1); + const max = seconds.length > 0 ? Math.max(...seconds) : 0; + console.log( + `baseline: ${routed.length}/${rows.length} routed` + + (unreachable.length > 0 ? `, ${unreachable.length} unreachable` : "") + + (unsnappable.length > 0 ? `, ${unsnappable.length} unsnappable` : "") + + ` — mean ${mean.toFixed(1)}s, max ${max.toFixed(1)}s`, + ); + + // Collect-then-write: the record is written ONLY after every pair measured. + const record = { + recordedAt: new Date().toISOString(), + tool: "routing/scripts/measure-optimality-baseline.mjs", + purpose: + "pre-ALT baseline for the D-03 optimality gate (Phase 8); measured BEFORE the ARG2 codec landed (Pitfall 1) — the record is immutable, delete by explicit git revert only", + epsilon: { name: "OPTIMALITY_EPSILON_PCT", pct: OPTIMALITY_EPSILON_PCT }, + graph: { + path: "routing/data/kl-roadgraph.v1.bin", + byteLength: buffer.byteLength, + nodeCount: graph.nodeCount, + edgeCount: graph.edgeCount, + argVersion: REQUIRED_ARG_VERSION, + }, + pairRules: { seed: `0x${BASELINE_SEED.toString(16)}`, count: BASELINE_PAIR_COUNT, minPairKm: MIN_PAIR_KM }, + pairs: rows, + }; + writeFileSync(RECORD_PATH, JSON.stringify(record, null, 2) + "\n"); + console.log(`wrote ${RECORD_PATH} — ${rows.length} pairs, epsilon ±${OPTIMALITY_EPSILON_PCT}% (the D-03 gate mirrors this constant)`); + process.exit(0); +} + +const invoked = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ""; +if (invoked === import.meta.url) { + await main(); +} diff --git a/routing/test/fixtures/alt-baseline.json b/routing/test/fixtures/alt-baseline.json new file mode 100644 index 0000000..b3f7c36 --- /dev/null +++ b/routing/test/fixtures/alt-baseline.json @@ -0,0 +1,1423 @@ +{ + "recordedAt": "2026-10-25T09:12:54.567Z", + "tool": "routing/scripts/measure-optimality-baseline.mjs", + "purpose": "pre-ALT baseline for the D-03 optimality gate (Phase 8); measured BEFORE the ARG2 codec landed (Pitfall 1) — the record is immutable, delete by explicit git revert only", + "epsilon": { + "name": "OPTIMALITY_EPSILON_PCT", + "pct": 0.1 + }, + "graph": { + "path": "routing/data/kl-roadgraph.v1.bin", + "byteLength": 15449343, + "nodeCount": 311325, + "edgeCount": 677068, + "argVersion": 1 + }, + "pairRules": { + "seed": "0x8a1", + "count": 100, + "minPairKm": 12 + }, + "pairs": [ + { + "pairId": "x000", + "origin": { + "lat": 3.222744, + "lng": 101.734926 + }, + "destination": { + "lat": 3.111966, + "lng": 101.804402 + }, + "straightLineKm": 14.534, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x001", + "origin": { + "lat": 2.996149, + "lng": 101.752217 + }, + "destination": { + "lat": 3.199028, + "lng": 101.643299 + }, + "straightLineKm": 25.596, + "seconds": 1975.5018691122532, + "verdict": "reachable" + }, + { + "pairId": "x002", + "origin": { + "lat": 3.193272, + "lng": 101.770517 + }, + "destination": { + "lat": 3.2466, + "lng": 101.617309 + }, + "straightLineKm": 18.013, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x003", + "origin": { + "lat": 3.001471, + "lng": 101.623764 + }, + "destination": { + "lat": 3.245507, + "lng": 101.799426 + }, + "straightLineKm": 33.418, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x004", + "origin": { + "lat": 3.164978, + "lng": 101.643447 + }, + "destination": { + "lat": 3.105091, + "lng": 101.769325 + }, + "straightLineKm": 15.481, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x005", + "origin": { + "lat": 3.218378, + "lng": 101.595953 + }, + "destination": { + "lat": 3.170729, + "lng": 101.766997 + }, + "straightLineKm": 19.715, + "seconds": 1682.0540265692398, + "verdict": "reachable" + }, + { + "pairId": "x006", + "origin": { + "lat": 2.92919, + "lng": 101.723599 + }, + "destination": { + "lat": 2.930847, + "lng": 101.612904 + }, + "straightLineKm": 12.294, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x007", + "origin": { + "lat": 2.924543, + "lng": 101.564331 + }, + "destination": { + "lat": 3.148195, + "lng": 101.731082 + }, + "straightLineKm": 31.005, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x008", + "origin": { + "lat": 2.998098, + "lng": 101.609632 + }, + "destination": { + "lat": 3.235556, + "lng": 101.576923 + }, + "straightLineKm": 26.653, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x009", + "origin": { + "lat": 3.202293, + "lng": 101.709661 + }, + "destination": { + "lat": 3.118718, + "lng": 101.792051 + }, + "straightLineKm": 13.04, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x010", + "origin": { + "lat": 3.240217, + "lng": 101.685914 + }, + "destination": { + "lat": 3.08351, + "lng": 101.732622 + }, + "straightLineKm": 18.18, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x011", + "origin": { + "lat": 3.238923, + "lng": 101.647051 + }, + "destination": { + "lat": 2.977265, + "lng": 101.744864 + }, + "straightLineKm": 31.056, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x012", + "origin": { + "lat": 2.96717, + "lng": 101.801901 + }, + "destination": { + "lat": 3.099526, + "lng": 101.715766 + }, + "straightLineKm": 17.552, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x013", + "origin": { + "lat": 3.089107, + "lng": 101.715404 + }, + "destination": { + "lat": 3.134286, + "lng": 101.569987 + }, + "straightLineKm": 16.909, + "seconds": 1721.4772635549307, + "verdict": "reachable" + }, + { + "pairId": "x014", + "origin": { + "lat": 3.199626, + "lng": 101.676596 + }, + "destination": { + "lat": 3.14985, + "lng": 101.567807 + }, + "straightLineKm": 13.286, + "seconds": 999.2484827190638, + "verdict": "reachable" + }, + { + "pairId": "x015", + "origin": { + "lat": 3.192355, + "lng": 101.673531 + }, + "destination": { + "lat": 3.234378, + "lng": 101.794751 + }, + "straightLineKm": 14.246, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x016", + "origin": { + "lat": 3.141708, + "lng": 101.636877 + }, + "destination": { + "lat": 3.096297, + "lng": 101.740304 + }, + "straightLineKm": 12.545, + "seconds": 1129.70539743267, + "verdict": "reachable" + }, + { + "pairId": "x017", + "origin": { + "lat": 3.230541, + "lng": 101.730888 + }, + "destination": { + "lat": 2.966789, + "lng": 101.62064 + }, + "straightLineKm": 31.78, + "seconds": 2128.7488392796367, + "verdict": "reachable" + }, + { + "pairId": "x018", + "origin": { + "lat": 3.079169, + "lng": 101.772907 + }, + "destination": { + "lat": 2.965537, + "lng": 101.678131 + }, + "straightLineKm": 16.444, + "seconds": 1344.6148583367467, + "verdict": "reachable" + }, + { + "pairId": "x019", + "origin": { + "lat": 3.124838, + "lng": 101.795273 + }, + "destination": { + "lat": 3.207893, + "lng": 101.679387 + }, + "straightLineKm": 15.838, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x020", + "origin": { + "lat": 3.132098, + "lng": 101.58984 + }, + "destination": { + "lat": 3.114674, + "lng": 101.755468 + }, + "straightLineKm": 18.491, + "seconds": 1602.046335970983, + "verdict": "reachable" + }, + { + "pairId": "x021", + "origin": { + "lat": 3.029592, + "lng": 101.774035 + }, + "destination": { + "lat": 3.042904, + "lng": 101.547204 + }, + "straightLineKm": 25.231, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x022", + "origin": { + "lat": 2.961404, + "lng": 101.70416 + }, + "destination": { + "lat": 3.072034, + "lng": 101.690205 + }, + "straightLineKm": 12.399, + "seconds": 1344.072348549962, + "verdict": "reachable" + }, + { + "pairId": "x023", + "origin": { + "lat": 3.228605, + "lng": 101.664707 + }, + "destination": { + "lat": 3.048146, + "lng": 101.704961 + }, + "straightLineKm": 20.558, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x024", + "origin": { + "lat": 2.968619, + "lng": 101.792315 + }, + "destination": { + "lat": 3.011027, + "lng": 101.677356 + }, + "straightLineKm": 13.609, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x025", + "origin": { + "lat": 2.952568, + "lng": 101.671167 + }, + "destination": { + "lat": 3.076189, + "lng": 101.677288 + }, + "straightLineKm": 13.763, + "seconds": 1338.0473207235336, + "verdict": "reachable" + }, + { + "pairId": "x026", + "origin": { + "lat": 3.223171, + "lng": 101.628647 + }, + "destination": { + "lat": 3.156533, + "lng": 101.808679 + }, + "straightLineKm": 21.317, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x027", + "origin": { + "lat": 3.238071, + "lng": 101.687637 + }, + "destination": { + "lat": 3.05387, + "lng": 101.705949 + }, + "straightLineKm": 20.583, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x028", + "origin": { + "lat": 3.163301, + "lng": 101.616562 + }, + "destination": { + "lat": 3.157281, + "lng": 101.743762 + }, + "straightLineKm": 14.138, + "seconds": 1213.9153044298291, + "verdict": "reachable" + }, + { + "pairId": "x029", + "origin": { + "lat": 3.151512, + "lng": 101.792535 + }, + "destination": { + "lat": 3.174911, + "lng": 101.638534 + }, + "straightLineKm": 17.295, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x030", + "origin": { + "lat": 2.947519, + "lng": 101.681798 + }, + "destination": { + "lat": 2.962186, + "lng": 101.573028 + }, + "straightLineKm": 12.188, + "seconds": 1198.0722634866834, + "verdict": "reachable" + }, + { + "pairId": "x031", + "origin": { + "lat": 2.922529, + "lng": 101.67288 + }, + "destination": { + "lat": 3.164763, + "lng": 101.684702 + }, + "straightLineKm": 26.967, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x032", + "origin": { + "lat": 2.941896, + "lng": 101.732747 + }, + "destination": { + "lat": 2.937913, + "lng": 101.622036 + }, + "straightLineKm": 12.302, + "seconds": 1517.3164304718375, + "verdict": "reachable" + }, + { + "pairId": "x033", + "origin": { + "lat": 3.219867, + "lng": 101.705465 + }, + "destination": { + "lat": 3.03741, + "lng": 101.722771 + }, + "straightLineKm": 20.379, + "seconds": 1595.3760295584798, + "verdict": "reachable" + }, + { + "pairId": "x034", + "origin": { + "lat": 3.142887, + "lng": 101.697914 + }, + "destination": { + "lat": 3.032822, + "lng": 101.594745 + }, + "straightLineKm": 16.763, + "seconds": 1175.2652063518763, + "verdict": "reachable" + }, + { + "pairId": "x035", + "origin": { + "lat": 3.12349, + "lng": 101.746607 + }, + "destination": { + "lat": 2.960434, + "lng": 101.797205 + }, + "straightLineKm": 18.982, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x036", + "origin": { + "lat": 2.991376, + "lng": 101.594125 + }, + "destination": { + "lat": 3.089112, + "lng": 101.673492 + }, + "straightLineKm": 13.992, + "seconds": 1017.476244263351, + "verdict": "reachable" + }, + { + "pairId": "x037", + "origin": { + "lat": 2.959388, + "lng": 101.779578 + }, + "destination": { + "lat": 3.21992, + "lng": 101.712209 + }, + "straightLineKm": 29.92, + "seconds": 2053.1093722041696, + "verdict": "reachable" + }, + { + "pairId": "x038", + "origin": { + "lat": 3.080187, + "lng": 101.767702 + }, + "destination": { + "lat": 3.214003, + "lng": 101.728216 + }, + "straightLineKm": 15.512, + "seconds": 1214.7661259621382, + "verdict": "reachable" + }, + { + "pairId": "x039", + "origin": { + "lat": 3.041277, + "lng": 101.663958 + }, + "destination": { + "lat": 3.170995, + "lng": 101.674755 + }, + "straightLineKm": 14.474, + "seconds": 1294.653773970902, + "verdict": "reachable" + }, + { + "pairId": "x040", + "origin": { + "lat": 3.198001, + "lng": 101.789685 + }, + "destination": { + "lat": 3.224131, + "lng": 101.624138 + }, + "straightLineKm": 18.607, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x041", + "origin": { + "lat": 3.154189, + "lng": 101.77412 + }, + "destination": { + "lat": 2.985216, + "lng": 101.550489 + }, + "straightLineKm": 31.138, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x042", + "origin": { + "lat": 3.077289, + "lng": 101.616614 + }, + "destination": { + "lat": 3.218997, + "lng": 101.611555 + }, + "straightLineKm": 15.767, + "seconds": 1205.8083974123, + "verdict": "reachable" + }, + { + "pairId": "x043", + "origin": { + "lat": 3.001831, + "lng": 101.736753 + }, + "destination": { + "lat": 3.171327, + "lng": 101.597776 + }, + "straightLineKm": 24.358, + "seconds": 1903.4196098931134, + "verdict": "reachable" + }, + { + "pairId": "x044", + "origin": { + "lat": 3.088539, + "lng": 101.736482 + }, + "destination": { + "lat": 3.208773, + "lng": 101.550705 + }, + "straightLineKm": 24.58, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x045", + "origin": { + "lat": 3.162648, + "lng": 101.692292 + }, + "destination": { + "lat": 2.985749, + "lng": 101.657187 + }, + "straightLineKm": 20.053, + "seconds": 1460.5746384188533, + "verdict": "reachable" + }, + { + "pairId": "x046", + "origin": { + "lat": 3.169152, + "lng": 101.700247 + }, + "destination": { + "lat": 3.062198, + "lng": 101.613261 + }, + "straightLineKm": 15.32, + "seconds": 959.943069005385, + "verdict": "reachable" + }, + { + "pairId": "x047", + "origin": { + "lat": 3.101287, + "lng": 101.689088 + }, + "destination": { + "lat": 2.993819, + "lng": 101.790104 + }, + "straightLineKm": 16.389, + "seconds": 1139.7499365955591, + "verdict": "reachable" + }, + { + "pairId": "x048", + "origin": { + "lat": 2.975643, + "lng": 101.740023 + }, + "destination": { + "lat": 3.19195, + "lng": 101.562565 + }, + "straightLineKm": 31.093, + "seconds": 2307.9911642596126, + "verdict": "reachable" + }, + { + "pairId": "x049", + "origin": { + "lat": 3.010139, + "lng": 101.617554 + }, + "destination": { + "lat": 2.945864, + "lng": 101.730565 + }, + "straightLineKm": 14.442, + "seconds": 1194.2127060443163, + "verdict": "reachable" + }, + { + "pairId": "x050", + "origin": { + "lat": 2.92224, + "lng": 101.790407 + }, + "destination": { + "lat": 3.159696, + "lng": 101.633952 + }, + "straightLineKm": 31.606, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x051", + "origin": { + "lat": 3.049968, + "lng": 101.605024 + }, + "destination": { + "lat": 3.208507, + "lng": 101.606206 + }, + "straightLineKm": 17.629, + "seconds": 1485.2963339202106, + "verdict": "reachable" + }, + { + "pairId": "x052", + "origin": { + "lat": 3.171879, + "lng": 101.72746 + }, + "destination": { + "lat": 3.016724, + "lng": 101.594038 + }, + "straightLineKm": 22.74, + "seconds": 1726.562555288896, + "verdict": "reachable" + }, + { + "pairId": "x053", + "origin": { + "lat": 3.14284, + "lng": 101.650223 + }, + "destination": { + "lat": 2.974496, + "lng": 101.562255 + }, + "straightLineKm": 21.114, + "seconds": 1788.0349795557559, + "verdict": "reachable" + }, + { + "pairId": "x054", + "origin": { + "lat": 3.057648, + "lng": 101.723788 + }, + "destination": { + "lat": 2.932228, + "lng": 101.735379 + }, + "straightLineKm": 14.005, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x055", + "origin": { + "lat": 3.167969, + "lng": 101.714426 + }, + "destination": { + "lat": 3.228092, + "lng": 101.602294 + }, + "straightLineKm": 14.131, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x056", + "origin": { + "lat": 2.936921, + "lng": 101.627435 + }, + "destination": { + "lat": 3.150116, + "lng": 101.73861 + }, + "straightLineKm": 26.728, + "seconds": 1671.7597011178732, + "verdict": "reachable" + }, + { + "pairId": "x057", + "origin": { + "lat": 3.045866, + "lng": 101.769056 + }, + "destination": { + "lat": 3.190677, + "lng": 101.672382 + }, + "straightLineKm": 19.352, + "seconds": 1225.4125319197774, + "verdict": "reachable" + }, + { + "pairId": "x058", + "origin": { + "lat": 2.932962, + "lng": 101.706202 + }, + "destination": { + "lat": 3.114297, + "lng": 101.739654 + }, + "straightLineKm": 20.503, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x059", + "origin": { + "lat": 3.201528, + "lng": 101.694151 + }, + "destination": { + "lat": 3.045405, + "lng": 101.798456 + }, + "straightLineKm": 20.868, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x060", + "origin": { + "lat": 2.926319, + "lng": 101.728264 + }, + "destination": { + "lat": 3.082534, + "lng": 101.569904 + }, + "straightLineKm": 24.717, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x061", + "origin": { + "lat": 3.107747, + "lng": 101.780408 + }, + "destination": { + "lat": 2.958853, + "lng": 101.639833 + }, + "straightLineKm": 22.754, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x062", + "origin": { + "lat": 3.121852, + "lng": 101.708761 + }, + "destination": { + "lat": 2.932841, + "lng": 101.77693 + }, + "straightLineKm": 22.339, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x063", + "origin": { + "lat": 3.113237, + "lng": 101.57123 + }, + "destination": { + "lat": 2.926203, + "lng": 101.801261 + }, + "straightLineKm": 32.939, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x064", + "origin": { + "lat": 3.139784, + "lng": 101.791372 + }, + "destination": { + "lat": 3.030598, + "lng": 101.763635 + }, + "straightLineKm": 12.525, + "seconds": 1057.2287946939468, + "verdict": "reachable" + }, + { + "pairId": "x065", + "origin": { + "lat": 3.06494, + "lng": 101.636926 + }, + "destination": { + "lat": 3.188721, + "lng": 101.618207 + }, + "straightLineKm": 13.92, + "seconds": 1243.9002071619034, + "verdict": "reachable" + }, + { + "pairId": "x066", + "origin": { + "lat": 3.03402, + "lng": 101.69311 + }, + "destination": { + "lat": 3.005677, + "lng": 101.581657 + }, + "straightLineKm": 12.771, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x067", + "origin": { + "lat": 2.946229, + "lng": 101.648418 + }, + "destination": { + "lat": 3.050986, + "lng": 101.785244 + }, + "straightLineKm": 19.145, + "seconds": 1408.0321597605944, + "verdict": "reachable" + }, + { + "pairId": "x068", + "origin": { + "lat": 3.216015, + "lng": 101.65629 + }, + "destination": { + "lat": 2.937126, + "lng": 101.728203 + }, + "straightLineKm": 32.023, + "seconds": 2047.1403729245067, + "verdict": "reachable" + }, + { + "pairId": "x069", + "origin": { + "lat": 2.95064, + "lng": 101.66846 + }, + "destination": { + "lat": 3.179271, + "lng": 101.687248 + }, + "straightLineKm": 25.508, + "seconds": 1973.4954812079668, + "verdict": "reachable" + }, + { + "pairId": "x070", + "origin": { + "lat": 3.00565, + "lng": 101.66477 + }, + "destination": { + "lat": 3.143815, + "lng": 101.613744 + }, + "straightLineKm": 16.375, + "seconds": 1659.940153747797, + "verdict": "reachable" + }, + { + "pairId": "x071", + "origin": { + "lat": 2.950224, + "lng": 101.807005 + }, + "destination": { + "lat": 3.098102, + "lng": 101.662583 + }, + "straightLineKm": 22.969, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x072", + "origin": { + "lat": 3.025497, + "lng": 101.674164 + }, + "destination": { + "lat": 3.174192, + "lng": 101.563685 + }, + "straightLineKm": 20.588, + "seconds": 1754.0856666527689, + "verdict": "reachable" + }, + { + "pairId": "x073", + "origin": { + "lat": 3.175377, + "lng": 101.748957 + }, + "destination": { + "lat": 2.970012, + "lng": 101.667672 + }, + "straightLineKm": 24.554, + "seconds": 1719.800110399723, + "verdict": "reachable" + }, + { + "pairId": "x074", + "origin": { + "lat": 3.186594, + "lng": 101.576665 + }, + "destination": { + "lat": 3.099667, + "lng": 101.774074 + }, + "straightLineKm": 23.955, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x075", + "origin": { + "lat": 3.014169, + "lng": 101.663034 + }, + "destination": { + "lat": 3.091264, + "lng": 101.759211 + }, + "straightLineKm": 13.694, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x076", + "origin": { + "lat": 2.94537, + "lng": 101.648079 + }, + "destination": { + "lat": 3.049277, + "lng": 101.793187 + }, + "straightLineKm": 19.827, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x077", + "origin": { + "lat": 3.159002, + "lng": 101.768414 + }, + "destination": { + "lat": 3.137695, + "lng": 101.595522 + }, + "straightLineKm": 19.341, + "seconds": 1553.086405813694, + "verdict": "reachable" + }, + { + "pairId": "x078", + "origin": { + "lat": 2.92376, + "lng": 101.750754 + }, + "destination": { + "lat": 3.026846, + "lng": 101.793298 + }, + "straightLineKm": 12.398, + "seconds": 1208.927390664816, + "verdict": "reachable" + }, + { + "pairId": "x079", + "origin": { + "lat": 3.029452, + "lng": 101.749969 + }, + "destination": { + "lat": 3.174084, + "lng": 101.660558 + }, + "straightLineKm": 18.9, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x080", + "origin": { + "lat": 2.955475, + "lng": 101.728828 + }, + "destination": { + "lat": 2.935294, + "lng": 101.598115 + }, + "straightLineKm": 14.688, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x081", + "origin": { + "lat": 3.138314, + "lng": 101.710875 + }, + "destination": { + "lat": 3.025817, + "lng": 101.583692 + }, + "straightLineKm": 18.865, + "seconds": 1340.462026240304, + "verdict": "reachable" + }, + { + "pairId": "x082", + "origin": { + "lat": 3.007028, + "lng": 101.744145 + }, + "destination": { + "lat": 3.08032, + "lng": 101.558379 + }, + "straightLineKm": 22.179, + "seconds": 1849.556734263897, + "verdict": "reachable" + }, + { + "pairId": "x083", + "origin": { + "lat": 3.00693, + "lng": 101.600644 + }, + "destination": { + "lat": 3.106587, + "lng": 101.753836 + }, + "straightLineKm": 20.301, + "seconds": 1484.5252708494663, + "verdict": "reachable" + }, + { + "pairId": "x084", + "origin": { + "lat": 3.240329, + "lng": 101.61098 + }, + "destination": { + "lat": 3.020593, + "lng": 101.776875 + }, + "straightLineKm": 30.598, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x085", + "origin": { + "lat": 3.240779, + "lng": 101.756042 + }, + "destination": { + "lat": 3.15109, + "lng": 101.557876 + }, + "straightLineKm": 24.156, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x086", + "origin": { + "lat": 2.927877, + "lng": 101.641239 + }, + "destination": { + "lat": 3.087665, + "lng": 101.612075 + }, + "straightLineKm": 18.06, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x087", + "origin": { + "lat": 3.173486, + "lng": 101.614754 + }, + "destination": { + "lat": 2.97929, + "lng": 101.770947 + }, + "straightLineKm": 27.696, + "seconds": 1968.9261868447065, + "verdict": "reachable" + }, + { + "pairId": "x088", + "origin": { + "lat": 3.156561, + "lng": 101.774491 + }, + "destination": { + "lat": 3.09866, + "lng": 101.56952 + }, + "straightLineKm": 23.651, + "seconds": 1624.6432969607413, + "verdict": "reachable" + }, + { + "pairId": "x089", + "origin": { + "lat": 3.048735, + "lng": 101.661635 + }, + "destination": { + "lat": 3.130127, + "lng": 101.80137 + }, + "straightLineKm": 17.962, + "seconds": 1346.9365817010403, + "verdict": "reachable" + }, + { + "pairId": "x090", + "origin": { + "lat": 2.931803, + "lng": 101.660382 + }, + "destination": { + "lat": 3.179515, + "lng": 101.612019 + }, + "straightLineKm": 28.063, + "seconds": 2086.70685569942, + "verdict": "reachable" + }, + { + "pairId": "x091", + "origin": { + "lat": 2.994353, + "lng": 101.582303 + }, + "destination": { + "lat": 3.101239, + "lng": 101.706162 + }, + "straightLineKm": 18.177, + "seconds": 1624.5921126492321, + "verdict": "reachable" + }, + { + "pairId": "x092", + "origin": { + "lat": 3.098155, + "lng": 101.559504 + }, + "destination": { + "lat": 3.052315, + "lng": 101.735075 + }, + "straightLineKm": 20.15, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x093", + "origin": { + "lat": 3.082357, + "lng": 101.694699 + }, + "destination": { + "lat": 3.173566, + "lng": 101.548069 + }, + "straightLineKm": 19.181, + "seconds": null, + "verdict": "unreachable" + }, + { + "pairId": "x094", + "origin": { + "lat": 3.185597, + "lng": 101.62181 + }, + "destination": { + "lat": 2.953825, + "lng": 101.684029 + }, + "straightLineKm": 26.682, + "seconds": 2006.0297141708434, + "verdict": "reachable" + }, + { + "pairId": "x095", + "origin": { + "lat": 3.164428, + "lng": 101.759038 + }, + "destination": { + "lat": 3.125818, + "lng": 101.551936 + }, + "straightLineKm": 23.391, + "seconds": null, + "verdict": "unsnappable" + }, + { + "pairId": "x096", + "origin": { + "lat": 3.20835, + "lng": 101.756027 + }, + "destination": { + "lat": 3.072036, + "lng": 101.725505 + }, + "straightLineKm": 15.532, + "seconds": 959.8540120869875, + "verdict": "reachable" + }, + { + "pairId": "x097", + "origin": { + "lat": 3.193684, + "lng": 101.715357 + }, + "destination": { + "lat": 3.061898, + "lng": 101.562843 + }, + "straightLineKm": 22.394, + "seconds": 1539.6350057888776, + "verdict": "reachable" + }, + { + "pairId": "x098", + "origin": { + "lat": 2.962919, + "lng": 101.741144 + }, + "destination": { + "lat": 3.000435, + "lng": 101.55843 + }, + "straightLineKm": 20.714, + "seconds": 1554.8940539509058, + "verdict": "reachable" + }, + { + "pairId": "x099", + "origin": { + "lat": 3.214983, + "lng": 101.578575 + }, + "destination": { + "lat": 3.233581, + "lng": 101.809189 + }, + "straightLineKm": 25.686, + "seconds": null, + "verdict": "unsnappable" + } + ] +}