From 4e89cff5523b2e86aff76b32b3a8a99be2b7624a Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Sat, 24 Oct 2026 05:17:01 +0800 Subject: [PATCH] feat(05-02): pinned golden-pair corpus + OSRM-demo fetch tool + populated committed dataset (GREEN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - routing/test/fixtures/golden-pairs.json: the hand-authored 15-pair pinned corpus (D-02/D-12) — origins AND destinations are gazetteer landmarks, klBbox + 100 KB gz budget + probe id + alternatives floor pinned - routing/scripts/fetch-route-fixtures.mjs: the maintainer build tool (overpass-topup.mjs pattern) — D-12 boundary in the header, pre-flight public-coordinate proof against the gazetteer (1e-4 deg), mandatory User-Agent, 1200 ms inter-request delay, one retry on network/5xx, collect-then-write, --only= single-pair repair merge - routing/src/data/route-fixtures.json: 15 real OSRM-demo pairs committed (1-3 verbatim polyline6 routes each; 8 pairs carry >= 2 alternates), ODbL header fields preserved (T-05-06) - loader-env L3 armed (RED commit 01b0ed6) + C6 re-pinned from the empty-seed state to the populated corpus (05-01 documented both as 05-02 handoffs) - npm delegates: root routing:fixtures; routing fixtures + corpus scripts --- package.json | 1 + routing/package.json | 4 +- routing/scripts/fetch-route-fixtures.mjs | 249 ++++++++++++++++++++ routing/src/data/route-fixtures.json | 283 ++++++++++++++++++++++- routing/test/contract.test.mjs | 12 +- routing/test/fixtures/golden-pairs.json | 24 ++ 6 files changed, 565 insertions(+), 8 deletions(-) create mode 100644 routing/scripts/fetch-route-fixtures.mjs create mode 100644 routing/test/fixtures/golden-pairs.json diff --git a/package.json b/package.json index ff20f20..89c6872 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "routing:test": "npm test -w routing", "routing:typecheck": "npm run typecheck -w routing", "routing:corpus": "node routing/scripts/route-corpus.mjs", + "routing:fixtures": "node routing/scripts/fetch-route-fixtures.mjs", "web:dev": "npm run dev -w web", "web:build": "npm run build -w theme && npm run build -w web", "web:build:harness": "npm run build:harness -w web", diff --git a/routing/package.json b/routing/package.json index 561ba0a..766779b 100644 --- a/routing/package.json +++ b/routing/package.json @@ -4,7 +4,9 @@ "type": "module", "scripts": { "test": "node test/runner.mjs", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "fixtures": "node scripts/fetch-route-fixtures.mjs", + "corpus": "node scripts/route-corpus.mjs" }, "devDependencies": { "typescript": "^5.9.3" diff --git a/routing/scripts/fetch-route-fixtures.mjs b/routing/scripts/fetch-route-fixtures.mjs new file mode 100644 index 0000000..f89dba9 --- /dev/null +++ b/routing/scripts/fetch-route-fixtures.mjs @@ -0,0 +1,249 @@ +#!/usr/bin/env node +/* + * fetch-route-fixtures.mjs — build-time fixture precompute from the OSRM demo (D-02). + * ------------------------------------------------------------------------------------ + * A MAINTAINER-TIME REPO TOOL in the overpass-topup.mjs pattern: it is NOT a + * runtime dependency. Nothing in bot/src/, web/src/, or routing/src/ imports + * it; no deploy, test, CI run, or vite build ever runs it. The app only ever + * reads the COMMITTED routing/src/data/route-fixtures.json. + * + * D-12 BOUNDARY: this script runs on the MAINTAINER'S machine against FIXED + * PUBLIC coordinates (gazetteer landmarks). No user coordinate can ever enter + * a fixture. The app itself gains NO runtime dependency on this server or any + * network. + * + * Mechanically: before ANY request leaves the machine, every corpus pair is + * validated against the committed gazetteer — the pinned origin must EQUAL + * the named landmark's coordinate (within 1e-4 deg) and the destinationId + * must exist. A non-public or unknown coordinate is refused pre-flight, with + * the pair named. + * + * Request shape (verified live 2026-10-24, 05-RESEARCH.md Finding 2): + * GET https://router.project-osrm.org/route/v1/driving/{lng},{lat};{lng},{lat} + * ?alternatives=2&overview=full&geometries=polyline6 + * - alternatives=2 -> up to 2 alternates (<= 3 total; RTNG-02's "up to 3") + * - overview=full + geometries=polyline6 -> the geometry string is stored + * VERBATIM (no float rounding, no re-serialization) + * - steps are NOT requested (turn instructions are Phase 6; D-10) + * - identifying header is MANDATORY on every request: + * User-Agent: araharah-fixtures/1.0 (build-time fixture precompute; repo: araharah) + * - 1200 ms delay between pair requests; ONE immediate retry per pair on + * network error or 5xx (a second failure is a hard fail for that pair) + * + * COLLECT-THEN-WRITE: results accumulate in memory; the committed file is + * written ONLY after every pair succeeds. Any per-pair hard failure + * (code !== "Ok", zero routes, exhausted retry) exits 1 with the failing + * pair named and the committed file UNTOUCHED. + * + * Usage: + * npm run routing:fixtures # full regeneration (15 pairs) + * node routing/scripts/fetch-route-fixtures.mjs --only= + * # single-pair repair, merged + * # into the committed file — + * # the other pairs never churn + * + * Exit codes: 0 all pairs fetched and the file written; 1 pre-flight + * validation failure, per-pair hard failure, or usage error. + */ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); // routing/scripts +const CORPUS_PATH = resolve(here, "..", "test", "fixtures", "golden-pairs.json"); +const GAZETTEER_PATH = resolve(here, "..", "..", "search", "src", "data", "kl-gazetteer.json"); +const FIXTURES_PATH = resolve(here, "..", "src", "data", "route-fixtures.json"); + +const USER_AGENT = "araharah-fixtures/1.0 (build-time fixture precompute; repo: araharah)"; +const OSRM_ROUTE_BASE = "https://router.project-osrm.org/route/v1/driving"; +const INTER_REQUEST_DELAY_MS = 1200; +const REQUEST_TIMEOUT_MS = 30_000; +/** Max coordinate drift between a pinned corpus origin and its gazetteer + * landmark — 1e-4 deg is ~11 m; anything larger is not the same point. */ +const ORIGIN_MATCH_TOLERANCE_DEG = 1e-4; +const MAX_ROUTES = 3; + +/** The file-template header (T-05-06): the ODbL attribution is part of the + * file shape, not an optional add-on. An existing committed file's own + * header fields are preserved verbatim on top of these defaults. */ +const HEADER_TEMPLATE = { + _license: "ODbL-1.0", + _attribution: "© OpenStreetMap contributors", + _notice: + "Route geometry © OpenStreetMap contributors, precomputed from the public OSRM demo server (router.project-osrm.org) at build time between fixed public landmark coordinates. ODbL v1.0 — https://opendatacommons.org/licenses/odbl/1-0/", + _source: + "OSRM demo server (router.project-osrm.org) — build-time fetch, never a runtime dependency", +}; + +function fail(message) { + console.error(`fetch-route-fixtures: ${message}`); + process.exit(1); +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +/* ————— inputs ————— */ + +const onlyPairId = (() => { + const arg = process.argv.find((a) => a.startsWith("--only=")); + return arg === undefined ? null : arg.slice("--only=".length); +})(); + +const corpus = JSON.parse(readFileSync(CORPUS_PATH, "utf8")); +if (!Array.isArray(corpus.pairs) || corpus.pairs.length === 0) { + fail(`corpus has no pairs: ${CORPUS_PATH}`); +} +if (!existsSync(GAZETTEER_PATH)) { + fail(`gazetteer not found at ${GAZETTEER_PATH} — the cross-workspace relative path is load-bearing (gazetteer-corpus.mjs precedent)`); +} +const gazetteer = JSON.parse(readFileSync(GAZETTEER_PATH, "utf8")); +const gazetteerById = new Map(gazetteer.entries.map((e) => [e.id, e])); + +/** The committed dataset as it exists right now (null on first run). */ +const existingFile = existsSync(FIXTURES_PATH) + ? JSON.parse(readFileSync(FIXTURES_PATH, "utf8")) + : null; +const existingPairsById = new Map( + (existingFile?.pairs ?? []).map((p) => [p.pairId, p]), +); + +/* ————— PRE-FLIGHT validation (D-12): nothing is fetched until every pair is + * proven to be a public landmark pair ————— */ + +const seenPairIds = new Set(); +for (const pair of corpus.pairs) { + const name = pair.pairId ?? "(missing pairId)"; + if (seenPairIds.has(pair.pairId)) fail(`duplicate pairId in corpus: ${name}`); + seenPairIds.add(pair.pairId); + if (onlyPairId !== null && pair.pairId !== onlyPairId) continue; + + const landmark = gazetteerById.get(pair.originId); + if (landmark === undefined) { + fail(`pair ${name}: originId "${pair.originId}" is not a gazetteer landmark — refusing to send a non-public coordinate`); + } + const dLat = Math.abs(landmark.lat - pair.origin.lat); + const dLng = Math.abs(landmark.lng - pair.origin.lng); + if (dLat > ORIGIN_MATCH_TOLERANCE_DEG || dLng > ORIGIN_MATCH_TOLERANCE_DEG) { + fail( + `pair ${name}: pinned origin (${pair.origin.lat}, ${pair.origin.lng}) does not equal ` + + `gazetteer landmark "${pair.originId}" (${landmark.lat}, ${landmark.lng}) within ` + + `${ORIGIN_MATCH_TOLERANCE_DEG} deg — refusing to send (D-12)`, + ); + } + if (!gazetteerById.has(pair.destinationId)) { + fail(`pair ${name}: destinationId "${pair.destinationId}" is not a gazetteer entry — refusing`); + } +} +if (onlyPairId !== null && !seenPairIds.has(onlyPairId)) { + fail(`--only=${onlyPairId}: no such pairId in the corpus`); +} +console.log( + `pre-flight OK — ${onlyPairId ?? `all ${corpus.pairs.length}`} pair(s) proven public against the committed gazetteer`, +); + +/* ————— the OSRM demo request (one per pair, 1200 ms apart, one retry) ————— */ + +async function requestRoutes(pair) { + const coordinates = + `${pair.origin.lng},${pair.origin.lat};` + + `${gazetteerById.get(pair.destinationId).lng},${gazetteerById.get(pair.destinationId).lat}`; + const url = `${OSRM_ROUTE_BASE}/${coordinates}?alternatives=2&overview=full&geometries=polyline6`; + const attempts = 2; // the initial request + ONE immediate retry + for (let attempt = 1; attempt <= attempts; attempt++) { + let payload = null; + let networkError = null; + let status = 0; + try { + const res = await fetch(url, { + headers: { "User-Agent": USER_AGENT }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + status = res.status; + if (res.ok) payload = await res.json(); + } catch (err) { + networkError = err; + } + if (payload !== null && payload.code === "Ok" && Array.isArray(payload.routes) && payload.routes.length > 0) { + return payload.routes.slice(0, MAX_ROUTES); + } + const retryable = networkError !== null || status >= 500; + const detail = + networkError !== null + ? `network error: ${networkError.message}` + : payload !== null + ? `code=${payload.code} routes=${Array.isArray(payload.routes) ? payload.routes.length : "none"}` + : `HTTP ${status}`; + if (attempt < attempts && retryable) { + console.log(` retry ${pair.pairId} — ${detail}`); + continue; + } + fail(`pair ${pair.pairId}: OSRM demo did not deliver a usable route (${detail})`); + } + fail(`pair ${pair.pairId}: unreachable`); // structurally unreachable +} + +/* ————— collect (ALL pairs) then write (only on total success) ————— */ + +const fetchedRoutes = new Map(); // pairId -> FixtureRoute[] +let first = true; +for (const pair of corpus.pairs) { + const isTarget = onlyPairId === null || pair.pairId === onlyPairId; + if (!isTarget) continue; + if (!first) await sleep(INTER_REQUEST_DELAY_MS); + first = false; + const routes = await requestRoutes(pair); + fetchedRoutes.set( + pair.pairId, + routes.map((r) => ({ geometry: r.geometry, distanceM: r.distance, durationS: r.duration })), + ); + const best = routes[0]; + console.log( + ` ok ${pair.pairId} — ${routes.length} route(s); best ${(best.distance / 1000).toFixed(1)} km / ${(best.duration / 60).toFixed(1)} min`, + ); +} + +/** Output pairs in corpus order: the --only target (or every pair in a full + * run) carries freshly fetched routes; every other pair keeps its committed + * routes UNTOUCHED (a single-pair repair never churns the other fourteen). */ +function mergedPairs() { + const out = []; + for (const pair of corpus.pairs) { + const fresh = fetchedRoutes.get(pair.pairId); + if (fresh !== undefined) { + out.push({ + pairId: pair.pairId, + origin: pair.origin, + destinationId: pair.destinationId, + routes: fresh, + }); + continue; + } + const committed = existingPairsById.get(pair.pairId); + if (committed === undefined) { + fail(`pair ${pair.pairId}: no committed routes to keep and none fetched this run — run a full regeneration`); + } + out.push(committed); + } + return out; +} + +const pairs = mergedPairs(); +if (pairs.length !== corpus.pairs.length) { + fail(`corpus/dataset drift: ${pairs.length} output pairs vs ${corpus.pairs.length} corpus pairs`); +} + +const file = { + ...(existingFile ?? HEADER_TEMPLATE), + ...HEADER_TEMPLATE, + version: 1, + originToleranceM: 200, + pairs, +}; +writeFileSync(FIXTURES_PATH, `${JSON.stringify(file, null, 2)}\n`); + +const multi = pairs.filter((p) => p.routes.length >= 2).length; +console.log( + `wrote ${FIXTURES_PATH} — ${pairs.length} pairs, ${multi} with >= 2 routes (alternatives floor: ${corpus.alternativesFloorPairs})`, +); +console.log("next: npm run routing:corpus # the offline gate over the committed dataset"); +process.exit(0); diff --git a/routing/src/data/route-fixtures.json b/routing/src/data/route-fixtures.json index f761479..39c45aa 100644 --- a/routing/src/data/route-fixtures.json +++ b/routing/src/data/route-fixtures.json @@ -5,5 +5,286 @@ "_source": "OSRM demo server (router.project-osrm.org) — build-time fetch, never a runtime dependency", "version": 1, "originToleranceM": 200, - "pairs": [] + "pairs": [ + { + "pairId": "kl-tower-to-kl-sentral", + "origin": { + "lat": 3.1528, + "lng": 101.7038, + "label": "Kuala Lumpur Tower" + }, + "destinationId": "kl-sentral", + "routes": [ + { + "geometry": "iml_Ekao~`E|DrFp@VvZrUzBdBlGtFdNzLb@x@Tz@HzACfADnALv@nBvFpC_CfAeAv@sAf@{AHaAEsAIgAs@wDyDuFgDcEsIiHmK_G}HgGuGgIaCgCcBqCyAiCOoAKiC@{BJiBPaCz@aH\\}CBiESuQLyFNyATeAp@mBj@y@|BmBlIsHxD_ErAmDAkAKeAu@eAo@k@cBU{AJk@Tu@`Ag@v@WhBSlBa@dBYd@i@p@cBh@iA?eBUs@Qq@SgCeAdAmD`AwDx@}BxDoItCmE|@eAfCkD`AcAxCgCvCaCtEeDpL{KfBoBt@uBzJbAzNz@hOzAxDMvDs@h@fB|C|OzIz`@VfA|@dGhAhOpBdXt@xJ~Blb@r@tK|AtUr@rGbArHxAhHhBrGbBzEdDhHdF`ItKtPrGhMtDtIvAnDrBbGpBtJ`ChO|QloA|BzNR`A?@fA|Eb@`BxAvDdBpDlBjDvEvI`CnDbBzBbDxCtCrC`I`HtHhFfEnArBLjF_@bByAvS{PbJkJjOsNbC{B`IyJzAgCnAeCjAmBpDcFl@]pAYlATvBbA`R|M`DfDjCxDbBhCdBbEf@~BPbAV~FJlE@rCBfDHlFtBlb@VhEr@nLt@dIr@hIn@lGl@fF|AzLjAlHfAfEnBjG~BbGnCzFhCtEtCnE`DdEhD|DhCxBx@t@rJlHtFtExFzErCrB|CzBhAr@zAZ`@NdFbEhJpHpBjBzB~AfCrAhBr@dDp@fEf@dCF~C?|CQfLq@dHe@`EYxF]|CQxE]vF\\vHv@vCZjDl@~GnApX~EfKrAxJb@jRp@~PNhYn@ba@Udn@]hID|FFXpUa@bXSdCs@bCyAtC}@pBw@bByN`\\k@tCKdB@vACLFjAvAzFjFbHhHtKzDxIrElNlCtL~Gfi@fNnoA`LjbAhBbPlJ|{@XfC~BlTX|BhGvEn@dFrBrIhEpNtGjFtEjB`GUtE_CvCiGlCoF{AoOdBwHjAyCtCwBpDqAhCGfCN~AZxBn@hElCnHfQvAfD|AtD`B`E~GhP|EhLxH|QxHxQ`AxAzA~@jBd@lBGhBIfB_@xEaAx@WnC}@jEqBtGw@~KqFvLaGzFsCtCwAbA[~BmDh@qBPgBCyAm@kBiB{DwDqH[o@{EsJu@aBsE}JiA_CmAoCiAiCi@kAqAoCiDaH}AaD", + "distanceM": 5200, + "durationS": 559.6 + }, + { + "geometry": "iml_Ekao~`E|DrFp@VvZrUzBdBlGtFdNzLb@x@Tz@HzACfADnALv@nBvFpC_CfAeAv@sAf@{AHaAEsAIgAs@wDyDuFgDcEsIiHmK_G}HgGuGgIaCgCcBqCyAiCOoAKiC@{BJiBPaCz@aH\\}CBiESuQLyFNyATeAp@mBj@y@|BmBlIsHxD_ErAmDAkAKeAu@eAo@k@cBU{AJk@Tu@`Ag@v@WhBSlBa@dBYd@i@p@cBh@iA?eBUs@Qq@SgCeAdAmD`AwDx@}BxDoItCmE|@eAfCkD`AcAxCgCvCaCtEeDpL{KfBoBt@uBzJbAzNz@hOzAxDMvDs@h@fB|C|OzIz`@VfA|@dGhAhOpBdXt@xJ~Blb@r@tK|AtUr@rGbArHxAhHhBrGbBzEdDhHdF`ItKtPrGhMtDtIvAnDrBbGpBtJ`ChO|QloA|BzNR`A?@fA|Eb@`BxAvDdBpDlBjDvEvI`CnDbBzBbDxCtCrC`I`HtHhFfEnArBLjF_@bByAvS{PbJkJjOsNbC{B`IyJzAgCnAeCjAmBpDcFl@]pAYlATvBbA`R|M`DfDjCxDbBhCdBbEf@~BPbAV~FJlE@rCBfDtCG~NItRSrNQlBA|ACjKMbBEnEAbBClCEzSe@jDGxDIzV[hGAdDDd@NTF`@Lv@d@z@`AfAjBvA~BhC~EVx@Jz@vAdQHhA@r@BhDCnCuAbSWzDK`Bk@xJoApScAjOWlDxCt@hD|@jCf@nKrBr@J~En@pKzAvKhBtCb@nDl@nARpPhD~A\\xGfAdG~@xDXzLk@`ED`Dl@|UjE\\NbC`AdJnJpW`X~BpAnJ}@bd@kE~GShECdJBvCPhQ~@ts@fCpZnA|p@dBzl@zD~GuA~HeA`D{@zCgBjBkCz@oCNwB[_IeBsKgCsGgB}AyBkAaCc@{CIgCXmC~@mC`C_BpD_Jn`@Gz@eI~_BQhBgAlQsB`QgCtNsCxJiDhJiOta@yFtKya@zf@iAbIdAbHtEjNvc@tkA~P|[v@zAbEjHzK|RrCdFr@pAzObYuf@hVuBlKJhDjA~C`BjExA`EnHfQvAfD|AtD`B`E~GhP|EhLxH|QxHxQ`AxAzA~@jBd@lBGhBIfB_@xEaAx@WnC}@jEqBtGw@~KqFvLaGzFsCtCwAbA[~BmDh@qBPgBCyAm@kBiB{DwDqH[o@{EsJu@aBsE}JiA_CmAoCiAiCi@kAqAoCiDaH}AaD", + "distanceM": 5707.7, + "durationS": 585.2 + } + ] + }, + { + "pairId": "kl-sentral-to-suria-klcc", + "origin": { + "lat": 3.1343, + "lng": 101.6869, + "label": "KL Sentral" + }, + "destinationId": "suria-klcc", + "routes": [ + { + "geometry": "_zg~D{un}`E}NkZsFcKkCgBe@QaFyAkEa@wFBaF~AsHfFuf@hVuBlKJhDjA~C`BjExA`EnHfQvAfD|AtD`B`E~GhP|EhLxH|QxHxQ`AxAzA~@jBd@lBGhBIfB_@xEaAx@WnC}@jEqBtGw@~KqFvLaGzFsCtCwAbA[lGSrCAbBFvAThA\\lAv@~C|BtQhTtDt@|AUlBc@xa@o\\zI}HtA}Ax@mCB}ABeB_@sEwHyIkCyCUW}KkM_CmCkQiSqB_CiFaG{DuD_EuEqPmQEGcBmB_WgXyJoKoEgFw@eAaCiF_@sBHoB\\_Bf@y@rAwA|ZcYbA{@~CoC`GkF~LiKrHoGrBgBpD_D|DcEpNgOzDcEnFmGnCuD~@eBv@cBvBmHpAaFd@wANg@r@wCpByIj@qDCmBs@sAeBqBoFoDeDaCgCgBqBgBaC}CwAeEsDaFuC}EiBmEwAaFs@uDeDa_@u@qHs@kEkAaFqBkHeAmCs@iByB{EeCsEyBiDkCoDuCmDuBuB{D_DiHwE_LkHiHeDcK_D}IiCaFaAkHeAgGm@cFQ{uBwLe[sAon@iD}[}BcJEmILqH`@kUnCcUvDkF^qFNwJDeKUiJa@}@rAiAdAwAx@}Af@{BV}BA{B]oBw@cBmAY]Y]i@aAa@gAa@yBE_CT}Bp@uBpAqBjByAlA_BfAcBz@iBn@qBNm@rCqQbBaLt@mELi@J[\\w@Xs@Pu@|CqVRgBF{@?_AIy@Qq@Qs@mD}KFwFFoM@oLRwDDwAFgAN_Ad@yAj@{@nAgA~AqAnMyDlEwAtGkDhG_C`Zc]`V{ZfGsHfKeMhZu^nGeKtH{OvDmKtByHlF}T|Jee@zJke@`FgRjFsQlJa]zMe[fK{RrKaPhOeShY_[z\\e]fRmQhb@m_@b\\oXx]iZhd@q`@~g@yf@jY{YnEiJ~HqKhOoObi@ok@zK{NbNwQbB}Bz@iA`KcP|GgKlGmJhAwBj@wATaAf@gDy@{DUuCcAaFgAoDuDiFoDoDmIqEqSaLaC{Au^mV_KeJcFwLoRoSy[ib@eM_OkN}M}QuNkjAgt@cnBupAkJqFmf@__@qn@ab@}SgNyJyGqE_CkDcBsJuDaJwCyQ}EgLkCaJqBsWuEuJoAiu@}G_T{C{VkE{_@_KwPuEyYsGsOkD{iAi[iAYaNeDkAY}NqDgNaDmEcAyLgFwKmCwbBw\\a_A}Qcg@cK{TgDmQgDcW{@oQJigA`Ca_@jBwFJce@ByG@{M?}b@RuL^wXhC{BZwBb@aZbKqYnK{n@~T_LbEoVfIqVfIyAlDyArDQpEbAvNjB|VR|E\\hIj@jHfAnMDxB`@vYCvg@f@doAa@tDiAfHqBpHgBbGk~@vsD}AhG{o@pjC", + "distanceM": 9894.4, + "durationS": 881 + }, + { + "geometry": "_zg~D{un}`E}NkZsFcKkCgBe@QaFyAkEa@wFBaF~AsHfFuf@hVqDfBuOpHgCb@kCKyBy@iOwUk@iBoAcEc@{FU{CUcDk@yTM_i@]eIEgFJ}CLkANsAP_BRs@j@{Ab@aAfAoCtAuDh@gDDyCG}BYkByIoUeIaNcDcBcFOsD`AoCvAiDnCeSxQoU|W}MzKsEpN_HjTwEjQcDbTg@tEqApPg@tJKtHIrMBbF?|Jf@~[fClwA~@zVm@bVkApRsDz^aAzLwCdRoEvPcCzGkEvKmIrRmKnQoZlb@_iAljAmV~VsMhLa\\hVaKdG_HtDmJrE}OjGg[tImO`DwHp@wHp@qDFaHJcIOqIe@eLaB{LiCkJyC{ToKoEaDcHcFc[}Yk[eZ{S_SoN{LsO{OoJ{GgKuFkUiJiY_J{L}BaNqAoIUwIDqHH}Ed@wEt@eL~BeLpCgKnD_JtDcTtJuT~JwIrCmJzCiN`DkNtBq^tC{Mf@wNNeKE{Oo@mKcAcPaByXoEgZoEmQkCwI_AwIq@{x@oDwd@uDc\\mFiWyFeMaEaVeI}OkH_NiHqJmIwJyIeEqFgDgFaDeFmCmFiEgK_D}IoFkU}FwWcCsLqB_I}DyK_EmIeF_IoGmIsI_I{FgFmHiEoJuDwKkDuIaBaJu@oGm@yGOkPLeR^_Rx@wd@fCm[`@{MKsJSkN_BsEe@sF_AuEaAmGiAaLmDkLeF{VaNcDuCeCmB}FkFcJgIuCuCaDoDaFoG}EcH_M_UiXkj@gf@gcAiI}QmDwG_EeG_EmGgEqFgEiF{EgF}JsJsPaPgOuNyAwA_UwRaGoFsJyHoDkCmCqBcBmAek@ag@gXcZcQ}VkGkLyCkGuDkI_FgN_Uiw@mG{TaAqCgGkQiDgJ{AaEuEqN}DiLUs@{FoRcAgD_Ju`@kKmb@iE{N_CkGaD}JcDiN_@kDcBwM_@uO`@sWfAeOpBuM`GcTfRed@zGiTbBgHjEqSbC}T`AyPdBy\\d@uGx@eIrM{nAbBsL|CmYfCgRjBsMtA}J`G{a@~Ey^|CgRxB_L~AyFdC_IhByEp@oB|LkY|DoIhAgCzDmHfEcI~HgN~HyLrK_OjIaKzLeOvd@ok@jOiR~fAuvAbdAiqAb]_b@~GcHjHiGzSqMzS_Lv]}Qtj@wVnZcNpE}BhEiCrL}HlIcIvGyIjJ}OfF}KbEwL~CeOpDyPhHa]v@aDfBmHzFyLxRkXfLcHzHmE`DqApKoDpTyH`}Cm_ArKgC|_@gHrCmD|BsAr_@oIvFsB~d@}JzGyAxGM`G^`KtA~G|F`FzLtBfY|Anl@j@lCfBd|@f@doAa@tDiAfHqBpHgBbGk~@vsD}AhG{o@pjC", + "distanceM": 11616.4, + "durationS": 924.6 + } + ] + }, + { + "pairId": "suria-klcc-to-bangsar", + "origin": { + "lat": 3.1578, + "lng": 101.7117, + "label": "Suria KLCC" + }, + "destinationId": "bangsar", + "routes": [ + { + "geometry": "ktv_Egu~~`EsAlFaDnGeF`EmGvEmWbSySjOiTfUiOb`@{n@xZ}FhBsGx@cJ?_C?{Ev@uB`BqA`CCzCJrBzBnLzCrMb@dQnDh\\fHvkAlCp^`CdOnCpMzIrYpLzXnNnUvJ|JdKvI|c@b\\xLnIn]jXv\\xXhDhB|ChApC\\jFVz`@_@vDKbEwA|CcBxCwChD_HuAoD_B}E_AoEiB}KCSsE}]_@cCIkEIqEXiFh@}REmF}DrA_FdBmEzAcGtC}CpBgBhAqD~ByCtB{ErD{BlB{HzHoE~EmD|E{HrLqGzLkEjJkLt\\cCdMs@vD_ErSq@vCy@rDq@xCU`B_CvKeA|DsFnYiBbM}@fHQ~AuB|T{@tVoBxg@kBbYGrBC~Ao@hRmA`KuCbMK`@wD~MaDzJoIrQKhCDtA`@zA^|@Rf@t@hBpA~BfJbDjExA`MfE`IlChKnDxP|FtDnA\\JlItC`@LhC|@nP|ExI|AhGj@xCHtCKhQiAxCI|Hw@h@`ELvA`@^|@~Cj@|EN`DHlATlEn@l`@@xBBnGuDrDAhRLf@JJPJb@F`M[lJ?jKJ~Q~@vJvAvOdEbV`EfStDrX`ErJnBbVvExVfEhd@hHh^`F|NbB|OfCtK`AhBPhD_@vA}@zAe@xASlBU|ABjFJhFd@fJ`AfB|@zA|@p@v@r@fAn@bB`@bB@rAA|Bi@~D[r@aArBuAz@o@`@_@fBKzCBbCf@nHp@nJXlXp@veAL`Vj@nM|@~QpDl^v`@vzBpExQvG~PzKnQzHbKfOhGdEn@bF@d@CfRyAzADbJqAjIuB`@kBQ}SC}W~@qNNsC|A_DfCif@vK{JvHmAbCv@v@n@d@j@tArC|@nEQlGo@lK", + "distanceM": 5214.7, + "durationS": 568.3 + }, + { + "geometry": "ktv_Egu~~`EsAlFaDnGeF`EmGvEmWbSySjOiTfUiOb`@{n@xZ}FhBsGx@cJ?_C?{Ev@uB`BqA`CCzCJrBzBnLzCrMb@dQnDh\\fHvkAlCp^`CdOnCpMzIrYzFfGxHdNfIzLzS`UdWbR|UdSfJrFlB~@|BPnCArC_@`@MnAc@XKdC_BtEoElBaDrB}CvByClHyIfIaItIaHlEwCjEqCx@i@bBiA|IwEbLkEnDuAjEkAlFeBfP_GlP}F\\IfD{AjAg@jG{DbE{DjCuCfCcDdFuJhDeJ`C}IfC{LNo@x@iCbA}Cj@cBbA_CnBsElCwFdEkGtAqBvDqEbIuJlPgSX]zBuBlCeCzEdD`C|A|MtHjQrJpB|@tTrJx@\\nOvFrAf@zOjFh@L~HpBrFfA`G~@dOjBzJbAzNz@hOzAxDMvDs@h@fB|C|OzIz`@VfA|@dGhAhOpBdXt@xJ~Blb@r@tK|AtUr@rGbArHxAhHhBrGbBzEdDhHdF`ItKtPrGhMtDtIvAnDrBbGpBtJ`ChO|QloA|BzNR`A?@fA|Eb@`Bo@nAgA`BgAvAkCvDqBhCyJbNcCvC_BjBoQjRvZjWTPz@t@fWhTfEzDhD|ChCjByBvB{FvFuJpJeAbA{AzAcCtByAnAcEpDiJ`IqA`AaBpAoJlHqB~Aa@ZqAbAcCjBeGvEqNjLqv@tn@}ChEiCzEuGrPiApH_DrVGrCOxIRtC`BrXx@bKPvBTjApBnFvBnDp@t@`HbI~DbHF\\ThApAdJf@nHp@nJXlXp@veAL`Vj@nM|@~QpDl^v`@vzBpExQvG~PzKnQzHbKfOhGdEn@bF@d@CfRyAzADbJqAjIuB`@kBQ}SC}W~@qNNsC|A_DfCif@vK{JvHmAbCv@v@n@d@j@tArC|@nEQlGo@lK", + "distanceM": 5389.1, + "durationS": 598.6 + } + ] + }, + { + "pairId": "mid-valley-to-bukit-bintang", + "origin": { + "lat": 3.118, + "lng": 101.6779, + "label": "Mid Valley Megamall" + }, + "destinationId": "bukit-bintang", + "routes": [ + { + "geometry": "qah}Dsk}|`E??IWA[v@iCLGTEV@zCz@l@Y~FkBb@]Xw@Ay@Wq@o@y@wLaJwN@{z@dJ}UhC{Ap@{CfByB~BcBzCaFvLuBrG{JdXoGbJgLvUyDdJEpCn@vEdAzEtCvMrAbEjA`LrHhLnNhMzRpKfSjFpGt@jOhBfWpA`PZjO?zLeCnDSfP_BrE}A~AiAxEgJ\\{@~@aIx@kEvAgFrAsCpAoArA_AxHuDzDkAb@MpHiAfFkBtBw@dDsCfAoB`AaFW}HoAmQsB_S_@qDaC_LcCsFkCwEmE{F{E{DeGiEiPkO_E}CeFqDgGsDaJyDuGoBkGy@_IWoRh@ue@tE}O|Aak@hHmKlAaw@|HsIxCaEbFu@fEKpDf@bEbBvCnLbQjFdNnG`HpAd@pA@hCExBm@bDiC~CoG~N{^lDuIzG_PjBgGtAcGToCEuCiC{YiDeCsC{@wDs@qQw@oJy@{Dg@gDa@yCa@_RiDuCaAmO{GyUiHiP{IyRqNiEgFqH}I{oAipBoGeKqb@qp@eXeb@yYyj@yFmM_GaP_Xkz@s[ybA}FcOwGcOaIiO{HcL}EkHc[yc@aSgYwBsCoQaVuT}YeDmEuFeHyE{FwC}CsDiDeF_EiDcCcC_B{IuE}FeC}FwBaG_BiGyA}G{AsJyB_IeBiPuD_NgDiN_DwIqBkEaAyFwAmCq@mCy@uCcAcCmAoBwA_DcCyD{DsDaFuC}EiBmEwAaFs@uDeDa_@u@qHs@kEkAaFqBkHeAmCs@iByB{EeCsEyBiDkCoDuCmDuBuB{D_DiHwE_LkHiHeDcK_D}IiCaFaAkHeAgGm@cFQ{uBwLe[sAon@iD}[}BcJEmILqH`@kUnCcUvDkF^qFNwJDeKUiJa@}@rAiAdAwAx@}Af@{BV}BA{B]oBw@cBmAY]Y]i@aAa@gAuDuAqBk@iCi@cBUkCKwGKcCIqCMuAKmHQuJMiCC{@EuDYqBOyBMeEYoAAg@?_@AsHDyEBcE@{DBsFB}DBkB@SCIAm@UwAm@}BiA]USK_@WYQ{GcEgFgDSO}EsEsGP_GFqC@qNL}DA{@@_BCaM_@{BCaFWoFMoCoDoDuF{HiNeBqE_DuKaBkGeAsFwAuJaCwPi@cHuD{q@YeHWoFOoFCoDCQa@yGkAmGaBmFk@sA_AmAcEsEoKsLyA{AyCkCIGKEoAu@mByA}@qBuAsEKcAAkAOcFhC_N`CqNdJcn@tBoNfDeUnDkUxA}GlBwOjByOjBwOrAyPvAaTLgBr@sJZ}HPsATiBXaBnAaDhB_FbA{B|@kBpBgEpKmSzJ{Rp@kAlL_WvDoHbMiWH_A@aAGaAM}@W}@Wm@KUsBkB{I_F}OyIiBUmA?kAVqB`B}@nBIN}@pBkAt@uAb@yAN}ACwMeH}@]aAi@]Wg@_@q@u@ePaSyAuBeJmMy@kAa@k@uMaRkJyLoJ_K}AiA{D{BwD}BoFiDq@s@QS_@y@i@{AEYC_@AkAZkDzE}Kr@eB~EyKrD}G~DiGdA_A~@m@nAi@tAi@fKeH|CqB[gAaAwBwKaOuO_Te@m@`EwC", + "distanceM": 8034.7, + "durationS": 753.7 + } + ] + }, + { + "pairId": "bukit-bintang-to-mid-valley", + "origin": { + "lat": 3.1466, + "lng": 101.7106, + "label": "Bukit Bintang" + }, + "destinationId": "mid-valley", + "routes": [ + { + "geometry": "ex`_E}r|~`EjE}Cx@c@~@_@r@UhAOx@?`AF`@f@b@n@rHvIzJfN~BdDz@nAxAN`AIpAQ`Bo@bKsGl@OhA@rOtSzAtBvDdFzNuJhBoAf@_@~CwBbCaBvDiCtJwGxMcJzCuB`MyJpFmA|DiAdCk@^Gn@Kj@i@hBeBr@_BsBgNmD}QeBoMPgGPcCt@k@v@K~@D|@bA\\hCXtEhApOr@fJ~@`EhDvQnCbNd@`ChBhJvH`e@p@zDzAtIpAxGn@zC|C`OLf@Lf@lDlN`D|MzA~DrAxDtKdR~OlXt@dArT`[nRjZnC`EtDzEvArBxBbDXb@h@t@lA`BdCfDvXv\\|BjCpErExFpE`FpC|F|BpI`CxHnAzIt@fQ|B|EhAvFlBtJtErJfGpGhDvDvBtXpO~F`DzJjG`QnJhEtBnDtAvGnBfCt@`M`CpC^~P^aEpL[~@yF~OcAbEqCvIgLpg@Sz@uCpMMh@iElRgA~EcH`V{DvEmExPuDjKkIvPaIbMeYf]cPbRy]bd@kL|MkHbIeBrBiB|BkCfEeCrFkDrKz@jEFrDJjDPrG^pHd@~Fz@fFtA~D`@x@hDnHfBxBxCfDdEbE|ExDdCnBrFvDvJxGf_@bXrCdB~JzH`KfLts@fCpZnA|p@dBzl@zD|Ih@zLx@dLz@|Hz@pJbBjI|BjNtElObJlItF`FzD`FzElCzClC`DlBhC`CvDpBrDhBbE|AlE`BpGtAfHbA`IfApKxA`OhAtIv@zEx@|CfAvCfAtB`B~BfCzCrDdDfDdCjDrBxBjApDlAnH~BjH`BdNpCjHxA~TjEtEv@bFx@dShDxB^lDn@hCl@dPpD~C~@nCbAtE`BrGhCjH`FvFtElEbEzGdInHvKl\\hg@~a@dn@zj@~z@xG`KzG|MpAxCrA|CtAbDpFtOhFfOvNhd@j\\fgAvF~NlGnNvDhHvJtQx{Av`CxRvZ`t@veAbKzKbLvJxPhJ|VlIzUzEf^hAfGK|LSxDIb\\s@zAEx@CpDC`GyBdPy@`BKv@uBDcCKmDuA}Fi@mBsAkBgE_DwDkAmDe@wEDyFZ}EhCyBnBa@`CUpD?bGOra@PdCJ`Bv@tI`BdNj@~EHzCYjDeAvDgAtCyApNuBrG{JdXoGbJgLvUyDdJEpCn@vEdAzEtCvMrAbEjA`LrHhLnNhMzRpKfSjFpGt@jOhBfWpA`PZjO?zLeCnDSfP_BrE}A~AiAxEgJ\\{@~@aIx@kEvAgFrAsCpAoArA_AxHuDzDkAb@MpHiAfFkBtBw@dDsCfAoB`AaFW}HoAmQsB_S_@qDuVw[oAqAeA}@uV}Oe@Gi@H{EjDaGhDu@Nu@Bm@G_M_GyL}F]]sCy@US", + "distanceM": 7602.4, + "durationS": 645.1 + } + ] + }, + { + "pairId": "pwtc-to-sunway-velocity", + "origin": { + "lat": 3.1768, + "lng": 101.698, + "label": "PWTC" + }, + "destinationId": "sunway-velocity", + "routes": [ + { + "geometry": "kw{`Eo~c~`E|@{@pEcB`CBpDFw@bVGtE@pH?lCHbFLxINbHNtDTtCV`DPlCRjBVdBZ|B^bBt@tCz@|BFN`BzDnA`CbCtE\\j@jDbGvD|DpCjChAz@|A~@nCxArJbFrLtDnFlBlEdApDdAhALlDVjDPrDDdDF`KB|CSlL}@fL{A|E_A~Bg@hGyA~GsArBeEN}@BaASeCk@yBoGkV[eAeRqq@_GaTW}@_CmJIiG]yFkAoMcAuMwEmb@gFwZmBiC}BcEwBcG?aFx@sFnA{DpBuC`EwCbDsA|C{@hOgQrHqQxFqSnBsHrB_I`CuLj@sCnCkRdAwGfAkClBkDdBy\\d@uGx@eIrM{nAbBsL|CmYfCgRjBsMtA}J`G{a@~Ey^|CgRxB_L~AyFdC_IhByEp@oB|LkY|DoIhAgCzDmHfEcI~HgN~HyLrK_OjIaKzLeOvd@ok@jOiR~fAuvAbdAiqAb]_b@~GcHjHiGzSqMzS_Lv]}Qtj@wVnZcNpE}BhEiCrL}HlIcIvGyIjJ}OfF}KbEwL~CeOpDyPhHa]v@aDfBmHzFyLxRkXfLcHzHmE`DqApKoDpTyH`}Cm_ArKgC|_@gHz^uGz\\wGrTgFhdAe[tNqErdAg^hYwKtJgDbVgJ|FiBpCo@dJ_BdKmAdAM`^m@hh@QlYHtC@jNWn_@yCfDEjyAyAbXf@tf@nJli@rKnsA|XxCr@b[pHnHfBfDv@j^hIjMlCnJzA`De@dC{@rCgBjANnDf@hPxBdVxDfObC|g@vJjFzAzh@bOxa@pMxBj@~IlCjC`AvK|ApARje@dFvCZ|Df@hV~CxHkM|D_LdIuVx@cCn@iCf@iCx@aEjBwJ`EyUpAgH|Go_@t@aDl@sCvC}NR}@pEkOxEiUvLyp@tK{n@tAoG|BwHrEuJfv@giA~L_T|PcXfEsH`FuJlBaEtGgNdIuPzAaDiCiAw@_@qCoA", + "distanceM": 7504.6, + "durationS": 620.3 + } + ] + }, + { + "pairId": "masjid-jamek-to-lake-gardens", + "origin": { + "lat": 3.1484, + "lng": 101.6953, + "label": "Masjid Jamek" + }, + "destinationId": "lake-gardens", + "routes": [ + { + "geometry": "yjc_Euo_~`Ed@_D`@wBEq@cBj@}@L_ACS@UEcN{HaIkFiIaGmEuCoDuCZ[rDmD~JgJdAaAyBeCmD}D}@cA{@aAcCtByAnAcEpDiJ`IqA`AaBpAoJlHqB~Aa@ZqAbAcCjBeGvEqNjLqv@tn@}ChEiCzEuGrPiApH_DrVGrCOxIRtC`BrXfAzEl@nAv@dABBv@~@lApAhFd@fJ`AfB|@zA|@p@v@r@fAhBhA~FpDd@NlD^`o@bIvi@|Fd[`Jl]|AzIOhQ_BdG{@tFmA`OcDnXkKbR{LhGiEhEyCjJ{ExFiBzIiChCe@pDm@vF_@nKUbGNtGb@bFt@hL~BrLvCjQjCxCx@pGnAbFdAfF`AdFp@hFj@dIFvNc@hM{@l\\kDtVwDlDy@jDkA~Ae@pBeAnB}AbB_CF_@J]L]z@aBlAqA|A_AhBk@nBUjBBfBT`Bh@xA~@jAlAx@|Af@dB\\dDIdB[~A{@pBsA~Au@j@eBx@mB`@oBJeBIcCi@qDXkBX_NvBiBXao@vHuM`AmNb@oGhAkDx@sFxB{AnCkAdFXLRRN^D`@E^Q\\YV_@Ja@@wHrHsGdMs@vC", + "distanceM": 2390, + "durationS": 256.7 + } + ] + }, + { + "pairId": "titiwangsa-to-petaling-street", + "origin": { + "lat": 3.1714, + "lng": 101.7016, + "label": "Taman Tasik Titiwangsa" + }, + "destinationId": "petaling-street", + "routes": [ + { + "geometry": "ocq`Eg}j~`ErKPlDHzHrAzB^n@LWvWIpD[|TSdAe@`A}@z@kAdAyFfFcAdBgA~B{AjE}@xFYjEShFWdDY|Dq@lCwCpHs@bAaBn@uA^kAXwAH_CWUA_F_@eCbBiA|AK`@o@lJL|At@|BtAtB`J`BpRpDtl@hLrDz@fG~@zFx@jKl@jIApM?bXDpjAExFD|G?pBHhPX~CD~CHvM\\zI\\bDHxFzAxITbL\\jOo@nABhCHvHPpCHhITfAB~ADhDPpRh@rOv@dH`@pALfVhBhCVhPhArDXpIl@zMbAlEXp[~BdHd@nCTpFb@x@HnFd@`MdCvGpBpAb@rCbAxIlCrDnAdOzEzDtAx^nMfGxBdC|@nXtJjA`@jFjBrFnBpAb@fFdBvDlAfJbDjExA`MfE`IlChKnDxP|FtDnA\\JlItC`@LhC|@nP|ExI|AhGj@xCHtCKhQiAxCI|Hw@h@`ELvA`@^|@~Cj@|EN`DHlATlEn@l`@@xBBnGuDrDAhRLf@JJPJb@F`M[lJ?jKJ~Q~@vJvAvOdEbV`EfStDpb@dLdK~BvI~BtKfC~LvB~i@~H``@zFhb@pFt_@dEz_@fElRdCzp@rHp}@vKl]|AzIOhQ_BdG{@tFmA`OcDnXkKbR{LhGiEhEyCjJ{ExFiBzIiChCe@pDm@vF_@nKUbGNtGb@bFt@hL~BrLvCjQjCxCx@pGnAbFdAfF`AdFp@hFj@jFx@tG^fGPjMm@zj@aGlGgAjJiBrLsEvK}GtEaEdFyGxEgHhEwSzIgn@\\eC?kAo@}BY]Y]i@aAa@gAa@yBE_CT}Bp@uBpAqBjByAlA_BfAcBz@iBn@qBNm@rCqQbBaLt@mELi@J[\\w@Xs@Pu@|CqVRgBF{@?_AIy@Qq@Qs@mD}KFwFFoM@oLyAiA_DwC_C{AgEi@}BQ}BIkMIyH@kKHcBAqPPgw@`Ew@@{ALCnCuAbSWzDK`Bk@xJoApS}AKiOaAya@iCsCQBw@@iALoKVsAd@eAPg@Fu@", + "distanceM": 5183.3, + "durationS": 460.7 + }, + { + "geometry": "ocq`Eg}j~`ErKPlDHzHrAzB^n@LWvWIpD[|TSdAe@`A}@z@kAdAyFfFcAdBgA~B{AjE}@xFYjEShFWdDY|Dq@lCwCpHs@bAaBn@uA^kAXwAH_CWUA_F_@eCbBiA|AK`@o@lJL|At@|BtAtB`J`BpRpDtl@hLrDz@fG~@zFx@jKl@jIApM?bXDpjAExFD|G?pBHhPX~CD~CHvM\\zI\\bDHxFzAxITbL\\jOo@nABhCHvHPpCHhITfAB~ADhDPpRh@rOv@dH`@pALfVhBhCVhPhArDXpIl@zMbAlEXp[~BdHd@nCTpFb@x@HzGyA|As@pAsA\\qAp@yAjCyLnEoSxG{ZrL{i@hDyNbCmKZqAhC}KrGsXb@mB`F}S|@wDlHc[hB}H|BwJhB}HnEdAjGvAjXjGdFjA|IpB`ATtZ|G~D~@nBb@|CXlCXbLtBrAXRDdATxF~@xBd@XFdB^bXxF~@jBrAXvBb@nBb@vMpC`Dp@dL`CzEbAl[vGxX~FdJlBpJpBzNzCdFdAlBuExFgLtHyPtEmE\\e@|BuCjBc@xEAdBTdHvAvWfF|JjDlD`BnMdGnMfJdGnEjEfEpKfLlIpJ~HzIbIbKdCbD~HfLjBpCzB|Dn@vApD`HfClGpAvDrEnO|@fDlDjLp@rBv@hBx@rBhA`CtAfCjAhBlBrChD|C|CtB|ChBpDlAbFjApCf@xCPhDNxCCzAEtJiB~VqC`OoAbIgArB_A`D}AvCkBlCwBVWlBiBnAyArD}AtAs@`Bu@vZjWTPz@t@fWhTfEzDhD|ChCjB`C|AdGjEtK~HfM~ITNxDdB`CpAj@\\VLd@BRAZEb@QzLuOfB}BpC{DbF_HzFiIzE{GxFmJrA_CtL_TzJyP|@e@dAUh@@lAFdGXjDJxAB~@CpBE|FMbDItCG~NItRSrNQlBA|ACjKMbBEnEAbBClCEzSe@jDGxDIzV[hGAdDDd@NTF`@Lv@d@z@`AfAjBvA~BhC~EVx@Jz@vAdQHhA@r@BhDCnCuAbSWzDK`Bk@xJoApS}AKiOaAya@iCsCQBw@@iALoKVsAd@eAPg@Fu@", + "distanceM": 4809.1, + "durationS": 503 + } + ] + }, + { + "pairId": "sunway-pyramid-to-pavilion-kl", + "origin": { + "lat": 3.0726, + "lng": 101.6069, + "label": "Sunway Pyramid" + }, + "destinationId": "pavilion-kl", + "routes": [ + { + "geometry": "uiqzDuypx`EA`B?t@uWScDQ{Ia@wFa@qC{@iCeBqAiC_AeCg@}Ek@sJMoGCsKC{J\\oUJoZGgGdAuP|CiHA}AFcYKeSs@uSeCmFy@_B]oBK_A_C}Q{Ni_BkD}a@_Aa\\a@sNeDgEkB}BmCaBmBaAuG}Cmn@G}[MsT@s`AUghB^{B@aq@\\uA@sWDiTSip@AgI?av@@cRPqGFs@@gGFyCBwAMmBo@qA{@cAo@gAs@o]|Bs@a@a@u@M{Bu@aOi@c_@DgDNsKVuQdCse@zBe][qJqA{`@uAqUkGss@aCo\\}AcYuGciAyE_f@gNogAaGoYiPmo@sRak@oNc]gRi_@aa@}p@sTu[qnCq}DeLiQktA_qB{a@eh@}j@gj@es@{j@u[{R{k@k[k^iO_VwI{WgIaXyHsr@iPubBqYqf@uJ}TiGcLaEyDuAmD}A_B}@kMkGgWaPwRePcHsGuJ_LuEcGgPmVwPgZi~Bk`FkUq`@{NwVc]}m@uIkQoP}`@oI}TmIcV}HqWyOmo@yDeNoFmQ_S_j@}r@w|A{i@qiA{c@y}@gj@cbAkf@uz@seAajB_U_c@wcAktB{n@yqAaZ{m@{h@qhAiN}_@_GuUyC{NqB{M{AqS[mOU__@AuWpCuzAJgGDwb@lDebBx@w]d@mg@zDqvCzBmv@TkRc@eZmAml@_@mOIwDg@gVMeGiAaz@mCya@cDm\\}D}m@wDec@mFoYeGaRuAgEaPwZgR{SsNqK{KiGkTcK}UiFoS_BwHEaQ@}ND{{@XiSFmH@y@?{KDgLCwKc@cUaCcWaEyUiHiP{IyRqNiEgFqH}I{oAipBoGeKqb@qp@eXeb@yYyj@yFmM_GaP_Xkz@s[ybA}FcOwGcOaIiO{HcL}EkHc[yc@aSgYwBsCoQaVuT}YeDmEuFeHyE{FwC}CsDiDeF_EiDcCcC_B{IuE}FeC}FwBaG_BiGyA}G{AsJyB_IeBiPuD_NgDiN_DwIqBkEaAyFwAmCq@mCy@uCcAcCmAoBwA_DcCyD{DsDaFuC}EiBmEwAaFs@uDeDa_@u@qHs@kEkAaFqBkHeAmCs@iByB{EeCsEyBiDkCoDuCmDuBuB{D_DiHwE_LkHiHeDcK_D}IiCaFaAkHeAgGm@cFQ{uBwLe[sAon@iD}[}BcJEmILqH`@kUnCcUvDkF^qFNwJDeKUiJa@}@rAiAdAwAx@}Af@aAjD{L|t@oBvF_DtE{J~IyKnMgF|DgFfC_FlCkH|B_KfCsVxCkXzCsLh@kXM{Hc@yJoAcSqEqGWkCe@aDo@iTeC{XgCeMaC}^}H}JwDyGaBcG_A}OaCqMaCw\\}EsBYaC_CYe@_@aBM{CP}AvFai@R_CFo@Di@H{B@Y`@sODkAB_BDaBDw@bBsOLqAp@}FTuBP{ABW`@_DxAkNhAqJHyC?{BM{@Ko@cBmJKg@m@gCeAiDo@kAw@{@{D}CwC_CeJuGiKqHoBaB}@s@iJoH]YeBwA_CoBYWeAcAmO_NsDyCcF{D[wAIa@?UBYNWtJsMP]DU?i@Em@O_AUm@i@{AeA{AmAiBsGoLkB{D{AeDa@yAa@_BmA}Fq@eDo@eD{@_GkFi`@sDcWeEyTi@{D}AkGw@cDuBuG{@oCuGyR_CqFqDsGi@gAiSc`@yBoGwAgFuAyGUaBoAgMiAoN_G}_AuA}R[sDqE}^{Ja_@kDeP]kBWkBw@gHIu@t@eC[sH?iADoBDqET{C\\gFb@aFXgCdA}JJs@@U`@oD~@cJNu@pA{JLoAHg@j@yDs@kB`BeKfBuKb@_CpA_H~DwW|@eGhAcJ\\{DzBsSVmKEkGOuFc@wJCsDoAwJwBiPk@sLYkFKaECgBIqOKgF?eC\\{DPwBb@_E`@sCh@wDfCeLxBoKhAmEnAkDbAgCbAkChG_P`BmDd@cAvBiDzBmCbCuCvAeB`J}Od[vQqZni@", + "distanceM": 17621.1, + "durationS": 1245.6 + } + ] + }, + { + "pairId": "central-market-to-merdeka-118", + "origin": { + "lat": 3.1454, + "lng": 101.6953, + "label": "Central Market" + }, + "destinationId": "merdeka-118", + "routes": [ + { + "geometry": "it}~Dwp~}`E\\gGbAoNDQJKPCzGRp@J\\JdEvC`G|Ev@lAlB`CtAGREr@Yl@k@fBoBgH}Fy@q@e@_@kIyHoCoDoDuF{HiNeBqE_DuKaBkGeAsFwAuJaCwPi@cHiFpCgBpAmInFiL~GeDpBmF`FeB~AeChBmCtA}CpAsDpAqCv@yCl@sA\\qAZ_FM?{BM{@Ko@cBmJKg@m@gCeAiDo@kAw@{@{D}CpC{DbF_HzFiIzE{GxFmJrA_CtL_TzJyP|@e@dAUh@@lAFdGXjDJxAB~@CpBE|FMbDItCG~NItRSrNQlBA|ACjKMbBEnEAbBClCEzSe@jDGxDIzV[hGAdDDLoAEqNCyDCiE@qOCoEGsDKcCCw@IcBIuAIyBE{BAg@GsE^aMV{ETyDVoDr@_GnAaENuEb@wNf@qKnAeGzB{Fp@kBrDsHjFgKfFaK~FgMf@iAlNeXvD_IjAmBbA}Ap@cAbG{JhBuCtNcY`AuB`@q@tEwJfOqTqAmEd@cDf@q@", + "distanceM": 1940.5, + "durationS": 249.9 + } + ] + }, + { + "pairId": "little-india-brickfields-to-aquaria-klcc", + "origin": { + "lat": 3.1306, + "lng": 101.6869, + "label": "Brickfields" + }, + "destinationId": "aquaria-klcc", + "routes": [ + { + "geometry": "{ra~Dman}`Ed^bb@bUbVw@r@cc@~_@cFfCaE~CkIoJ{BgCiFcGsGqHuDiEqA{AaMsNwHyIkCyCUW}KkM_CmCkQiSqB_CiFaG{DuD_EuEqPmQEGcBmB_WgXyJoKoEgFw@eAoGmE_B_A}EqCqY{OcIoF_UcNa^qUsYmRiRqMsAgAc@i@_@i@oAqDmBoFmKq]{C{IiEiOcAyDqA_F_K}UuCeF{CmFa]gq@wIsNiM_OgPqMiOoJ}DwBuEiB_DgA}Cy@uDq@sCc@wJcAs[qAqb@e@aCCsA@e@?cEQoLe@_G]cGm@kHuAsU}EiP}B_Ia@iKQaMFeEFe@?mJd@{K`@_LKsDSoDe@aEcAoC_AyA[q@Ya@]m@i@oB{CIIeViSc@YkG_E{HeGsBeBgH}Fy@q@e@_@kIyHoCoDoDuF{HiNeBqE_DuKaBkGeAsFwAuJaCwPi@cHiFpCgBpAmInFiL~GeDpBmF`FeB~AeChBmCtA}CpAsDpAqCv@yCl@sA\\qAZ_FM?{BM{@Ko@cBmJKg@m@gCeAiDo@kAw@{@{D}CwC_CeJuGiKqHoBaB}@s@iJoH]YeBwA_CoBYWeAcAmO_NsDyCcF{D[wAIa@?UBYNWtJsMP]DU?i@Em@O_AUm@i@{AeA{AmAiBsGoLkB{D{AeDa@yAa@_BmA}Fq@eDo@eD{@_GkFi`@sDcWeEyTi@{D}AkGw@cDuBuG{@oCuGyR_CqFqDsGi@gAiSc`@yBoGwAgFuAyGUaBoAgMiAoN_G}_AuA}R[sDqE}^{Ja_@kDeP]kBWkBw@gHIu@t@eC[sH?iADoBDqET{C\\gFb@aFXgCdA}JJs@@U`@oD~@cJNu@pA{JLoAHg@j@yDs@kB`BeKfBuKb@_CpA_H~DwW|@eGhAcJ\\{DzBsSVmKEkGOuFc@wJCsDoAwJwBiPk@sLYkFKaE}MyUsAgAeAq@kA]_Ca@gFX_P|@]BoG\\wJz@yB^uBf@w@`@uBfAeDdBoAn@iKlFMFmDfBoBfAoAr@k@ZYP{Az@sA~@eGpIwGpO{@nB{C`HqSv]gErC{BrE", + "distanceM": 4999.8, + "durationS": 482.3 + }, + { + "geometry": "{ra~Dman}`Ed^bb@bUbVw@r@cc@~_@cFfCaE~CkIoJ{BgCiFcGsGqHuDiEqA{AaMsNwHyIkCyCUW}KkM_CmCkQiSqB_CiFaG{DuD_EuEqPmQEGcBmB_WgXyJoKoEgFw@eAaCiF_@sBHoB\\_Bf@y@rAwA|ZcYbA{@~CoC`GkF~LiKrHoGrBgBpD_D|DcEpNgOzDcEnFmGnCuD~@eBv@cBvBmHpAaFd@wANg@r@wCpByIj@qDCmBs@sAeBqBoFoDeDaCgCgBqBgBaC}CwAeEsDaFuC}EiBmEwAaFs@uDeDa_@u@qHs@kEkAaFqBkHeAmCs@iByB{EeCsEyBiDkCoDuCmDuBuB{D_DiHwE_LkHiHeDcK_D}IiCaFaAkHeAgGm@cFQ{uBwLe[sAon@iD}[}BcJEmILqH`@kUnCcUvDkF^qFNwJDeKUiJa@}@rAiAdAwAx@}Af@{BV}BA{B]oBw@cBmAY]Y]i@aAa@gAa@yBE_CT}Bp@uBpAqBjByAlA_BfAcBz@iBn@qBNm@rCqQbBaLt@mELi@J[\\w@Xs@Pu@|CqVRgBF{@?_AIy@Qq@Qs@mD}KFwFFoM@oLRwDDwAFgAN_Ad@yAj@{@nAgA~AqAnMyDlEwAtGkDhG_C`Zc]`V{ZfGsHfKeMhZu^nGeKtH{O~@yL~SkeAz@mEbGqZ`AkEhFcVo@wIoBmKcB{Hq@uBkHsBqHmEoPuNwL_O_\\uRyKuG}JqGiEsBaI{CyDaAaI_BmEe@oE]yTaCmCo@wBg@{DiAiBy@uJ}EoGiF_HkH}BiCuCoDwOqSwBgCsG_HyCiE_Uo]oXga@uBgEoOaW_JaOcA}BeA}B{CmIiA_FyAeHcEiRQy@wCwNc@wBeAoEq@cEs@}DOw@k@_D[_Be@wBaAkEmDePOy@[cB[gB[gBwD{ScA}GsBgNmD}QeBoM}@cAu@y@}AwB_B_BsAiAWO}Ae@s@KcDn@cYpGmNfDmg@rKm]pHc@JeMlCq^rGuCt@oCj@sCp@}AwBe@FmARaC^{QrCoIvAyCh@uGxAgCVaLnAiJ^qNByIc@c`@yCyOmAsJBmN|@yEpAaFfBsDb@eEGuCi@iC{CwBiPk@sLYkFKaE}MyUsAgAeAq@kA]_Ca@gFX_P|@]BoG\\wJz@yB^uBf@w@`@uBfAeDdBoAn@iKlFMFmDfBoBfAoAr@k@ZYP{Az@sA~@eGpIwGpO{@nB{C`HqSv]gErC{BrE", + "distanceM": 6039.8, + "durationS": 521.8 + } + ] + }, + { + "pairId": "national-museum-to-pudu-raya", + "origin": { + "lat": 3.1378, + "lng": 101.687, + "label": "National Museum" + }, + "destinationId": "pudu-raya", + "routes": [ + { + "geometry": "arp~Dqjn}`E]}OM{Es@}CiENqBl@cB|B?|Jf@~[v@hKn@tF\\hL^|M|DzIvD~GdGpEfIxBnHf@lHGxJ_AbEo@bEuAzD_CfCkDnAyCb@gD^iLP_BGw@yD{v@c@_]qHqn@sGmj@gEg^_AuIkGyTqFy`@{BaHaD}EoE}DmGqAcHXqGxA}D|EmBdGOdIxApH`FpGnGdD`IAbEUbF{BtXiVfb@yd@nl@ql@~K{NtKwRvHgT|C_MxBsMrC_RNkC`McjBnEub@jDyRlFkQrJcQdK{KrJkK`FmD|G{E~|@{_@xQcMlNsJnJaNfGaMzFqOjHm^bCaNpC}XrG__@@yMc@kFaAmE}AiEsBcDyDwBcPeIuRiL_A[ym@yYyYmNwBcAkQqImIeE}DqBsBeAmFmCeNqH}JoFgM_HyGqDa@YaCuAiDqBmEmAo[PcE?_FQeQ}AfEsJfBeEnFeN`G{MtCuF|F_LrCgE|D_GfIsJfP_PxByBhFqFhKmLbH}G`e@ib@xDkFl@oBJc@Fi@[yLKuDImD_@wP{@ka@a@eQoA@{ABs@@a[XiHZwI`@gKzAwCn@iAVsElAyJvBgH|AwOjDcE|@}LlCqVnFuBd@yJub@}HvCqHzDkPrSkOlRcFlG", + "distanceM": 4450.1, + "durationS": 443 + }, + { + "geometry": "arp~Dqjn}`E]}OM{Es@}CiENqBl@cB|B?|Jf@~[v@hKn@tF\\hL^|M|DzIvD~GdGpEfIxBnHf@lHGxJ_AbEo@bEuAzD_CfCkDnAyCb@gD^iLP_BGw@yD{v@c@_]qHqn@sGmj@gEg^_AuIuQizAgEuYcBqIcB_ImBuFwBoEeDwEcDaEmFoFsDsC_E_D}Bs@wKqEsBw@}RoFeBc@wLoCau@qPcEe@iCAwBDcDf@oDt@u@j@eBx@mB`@oBJeBIcCi@wAq@kAaA_AkAo@wA_@_BMcBBcBVaBF_@J]L]z@aBlAqA|A_AhBk@nBUjBBfBT`Bh@xA~@jAlAx@|Af@dBxF|CnCjAvDjAdE`A~GzApQdEfYfGnDn@zHfBnCf@pHdBzKFnB@vB?lAE|@KnAYrAg@rA{@~@gBbNk]n@{AbAcCp@yC`@gELgVHgIE}@[{HUwBCcADu`@oAqC}@gAmAo@sAM}BFeVjDoHLeIt@eD@sEq@kF^qFNwJDeKUiJa@}@rAiAdAwAx@}Af@{BV}BA{B]oBw@cBmAY]Y]i@aAa@gAa@yBE_CT}Bp@uBpAqBjByAlA_BfAcBz@iBn@qBNm@rCqQbBaLt@mELi@J[\\w@Xs@Pu@|CqVRgBF{@?_AIy@Qq@Qs@mD}KFwFFoM@oLRwDDwAFgAN_Ad@yAj@{@nAgA~AqAnMyDlEwAtGkDhG_C`Zc]`V{ZfGsHfKeMhZu^nGeKtH{O~@yL~SkeAz@mEbGqZ`AkEhFcVrGkRtCqIp@qBfEsJfBeEnFeN`G{MtCuF|F_LrCgE|D_GfIsJfP_PxByBhFqFhKmLbH}G`e@ib@xDkFl@oBJc@Fi@[yLKuDImD_@wP{@ka@a@eQoA@{ABs@@a[XiHZwI`@gKzAwCn@iAVsElAyJvBgH|AwOjDcE|@}LlCqVnFuBd@yJub@}HvCqHzDkPrSkOlRcFlG", + "distanceM": 4672.7, + "durationS": 462.6 + } + ] + }, + { + "pairId": "mid-valley-to-ioi-city-mall", + "origin": { + "lat": 3.118, + "lng": 101.6779, + "label": "Mid Valley Megamall" + }, + "destinationId": "ioi-city-mall", + "routes": [ + { + "geometry": "qah}Dsk}|`E??IWA[v@iCLGTEV@zCz@l@Y~FkBb@]Xw@Ay@Wq@o@y@wLaJwN@{z@dJ}UhC{Ap@{CfByB~BcBzCaFvLuBrG{JdXoGbJgLvUyDdJEpCn@vEdAzEtCvMrAbEjA`LrHhLnNhMzRpKfSjFpGt@jOhBfWpA`PZjO?zLeCnDSfP_BrE}A~AiAxEgJ\\{@~@aIx@kEvAgFrAsCpAoArA_AxHuDzDkAb@MpHiAfFkBtBw@dDsCfAoB`AaFW}HoAmQsB_S_@qDaC_LcCsFkCwEmE{F{E{DeGiEiPkO_E}CeFqDgGsDaJyDuGoBkGy@_IWoRh@ue@tE}O|Aak@hHmKlAaw@|HsIxCaEbFu@fEKpDf@bEbBvCnLbQjFdNnG`HpAd@pA@hCExBm@bDiC~CoG~N{^lDuIzG_PjBgGtAcGToCEuCiC{YUaFEyAa@a`@[cIGkGt@uCtAyBzA_AxBc@nAMzACtAn@xF[vEElDd@vDjAfE~CrAjBh@lBtA|F~B`FjD~Cj`@hDvFB|F}@xEIlObAdNxA`DBbDTvAZxD|@~HjBtXxFpI|@lEp@rKbBtL`AxH`@xGCvL{AfJsEbJiM|FeJnO{VhY{]bIgKxFeJhBuC`JkRnHqOrRmj@vMqj@nGcd@bD_n@~CkoAxDwj@jD}S~EaV|FaRpG_Q|HoQ|JcQtLwPvNuQ`|B{zChd@gd@bYcTrRaLnYeNllBmdAzGuDpEcCzL{Hr\\sVbLaLtFoI`F{MhFeK|FgThDeUp@_H|DmGxBsWjC}PhDgPpFwTnEiM`DsHfJyNbFsF`GyE|N}IvOmG`LwChHoA|Iu@~MEvIRvJx@~NhCxK`DnJpEvG~DnH~FlGfH`G`ItDvHbDxIpCfLtCxRpClOt@jChFbN~ErH|GvH`J|H|EfDnEvFtOlFbW`FbQfCba@jEjg@pDfVnAnPd@hN`@xNVvw@dF`T[pK]dT}@nRuAfVcCtSeDtSkCrXcEpXiFdWyFrkAk\\zSiGdKqDrOmFnM}DbK_Dvj@oP`PqEvWqHpeAuYlh@qPbRaI`Q{IzLyHhQgLhKcJ|NoPlKgP|FmJjZyh@nGgMzRk^`FuH`GwIvFmHbH{HxIyIhJaI`JoHfJwGjKyGbLmGhK}EfMwFzJyC|WcHzKgCbLsBrMqAhHs@jV{AtOM|PHhNTlObAjh@vGz~AlYt_@dFlOtAzO|@`ZTf\\q@`~@uD~uAoGbsAuEfj@mBzh@wAzlD{If]oBfe@{Dlj@qF~U_C`f@yEpcAuPrRsChe@sFvW_EnAK~X_Cb_AaNvHiAvJqAnTeC|e@kE~WaCtVaBzpA_GniEiHnWYbv@mApN[`LQlDEfMSnoAmBz{@qAr_F_HteAgBpXm@hl@wBjf@kDvm@mE~j@aEnr@yCjKU~KIbUVj_@bAhLVjJRf[v@dUFvZHzqB{@`XcAj{@uFhMuAfTaCrOuBbPoCjMeCl\\wIbf@_OfPyF|PcH|{@ya@hPsJtMuIru@ig@vf@qe@dQsQjuAebBrlDo`EdXcY`q@ij@~IcGnnAyt@l~FauChbHcjDliFwhCbfAei@jvAmr@p^uRpd@aWpSsOb]qWtJyIvLqL`V_UlGwGnNcOlFoGvHaJ`BqBzd@ip@x]gg@dIwLp_BcjC|c@wn@d`AedAvqAefAxq@ua@v}IscExEuBjv@y\\ls@_b@~a@sUvk@e[nLoH`HuI~E{IlFmLtDsM|BqQj@mQp@iRqBss@o@ySYe[?aB?sABwENsEToFbBuSt@sGzByRScG`BiMzFcW`IaVtGaNnIoPnH{KbLuN|FcHzB}BxMsOvUoP|LcG|SgLnFaDfIeAmEjIsTvL_K`HuIlH_IbIaYfX_NzN{IpKmJdQiIxOaHjT{ElVyBxSo@j\\BnJKbVAtD?xAA|Ac@fZWxRo@jVo@vjB[~|A]nOcBdSyHpTiN|LwPnC}OUmTmEk[}LaOoFkScCsYlBgRxFyvAdk@", + "distanceM": 23134.4, + "durationS": 1528 + }, + { + "geometry": "qah}Dsk}|`E??IWA[v@iCLGTEV@zCz@l@Y~FkBb@]Xw@Ay@Wq@o@y@wLaJwN@{z@dJ}UhC{Ap@{CfByB~BcBzCaFvLuBrG{JdXoGbJgLvUyDdJEpCn@vEdAzEtCvMrAbEjA`LrHhLnNhMzRpKfSjFpGt@jOhBfWpA`PZjO?zLeCnDSfP_BrE}A~AiAxEgJ\\{@~@aIx@kEvAgFrAsCpAoArA_AxHuDzDkAb@MpHiAfFkBtBw@dDsCfAoB`AaFW}HoAmQsB_S_@qDaC_LcCsFkCwEmE{F{E{DeGiEiPkO_E}CeFqDgGsDaJyDuGoBkGy@_IWoRh@ue@tE}O|Aak@hHmKlAaw@|HsIxCaEbFu@fEKpDf@bEbBvCnLbQjFdNnG`HpAd@pA@hCExBm@bDiC~CoG~N{^lDuIzG_PjBgGtAcGToCEuCiC{YUaFEyAa@a`@[cIGkGt@uCtAyBzA_AxBc@nAMzACtAn@xF[vEElDd@vDjAfE~CrAjBh@lBtA|F~B`FjD~Cj`@hDvFB|F}@xEIlObAdNxAx[`JbPhFbRlF~JvAdq@fBl\\`DtWjCzg@dEpYdEjbAdMbRlBvg@pD|a@KjYgAvQ}AlBMlHg@j[gBr^sBhIm@jNq@xEKnHQdBEzJUbGEbGNbDP^BjJx@hALdf@~EzQfBzPfBdGn@tGd@zL~@bMhAxP`B`T|Bhp@jJlPtBhSbFrGtApLhCth@~Ph~A|l@bAVjOpDnRlCxNxB`[`EzM`BjLhAdPlA`PdAlAJn_@zCxRxBvEh@fY`DdLrA~OjBl`@pElZxD|[dEnZzD|nA~OzARdLxAdKpA`Cd@vBV`N|AhN|AbRpB|GoAbD}@v@g@nDuBj@a@`EeFv@eTf@uM`@cPLcCl@cK~AkKpB_HRy@z@kCvAoDlBmCbHeKtCkDdDoEbByCfAeE?}EK{Ci@{H_BkUe@kY@{KH_StBmq@BqRUsGIw@c@_L}AqEaAeGyAyFaBsFeBaEo@mEiKoNiAyAw`@ki@{DkGuQyZeC}HEUo@eD}@qEy@cKCwCEsCBeDf@oGbA}K~BcTbFab@p@yG\\wBzCy[T_Cp@kH|Ck]`@wFdAgNL}AlBkRf@{FPyLHaGm@eS}FqsAmBcu@EyAKuJLyDNiF^cFxAoLnDeOzAsEzFmNrEoIpCaE`EsEdE{D|JgIrNiKlOcMzIcJrDwDzSoM`J_ErJ}CrGuAhHcA`Gi@tHWnLBjKL|Tb@bIY~I{@xHyB|FyCxH_F`I}G~E}F`PmSzb@gi@bCmCto@eo@zs@ux@hc@ea@|OoKvLaIbM}CdP{E`F_BxKgHjJqJjFkNbB}Il@uGJ}Jr@ct@tAkR|DwSfGuQfFyOlCaHpDsI`DuHzEmIjFeIxNoPdHgHz@y@bIeHzHeF`Bw@lDaBrE{AhHqB|FqAzQgEtKmChAYzFkBrFgCfGsEnFmFdFkGxPuShJeLjIaKbKuMzCmDrJyL|DqDjEeDbIuDzHqC|JcB`BI`FUlJAhGh@|Fz@nJ`CvH|C|ZfTlPxI~KlE~I|CpK`CjJ|@lLf@xMC`OuAxMkCnOaF~GeDzb@sXtTsN|^iR~WyJt\\iJhNiCzXcEl[mC~PiBbKkAvGu@hF_AtL}C`SgFnVwFlUcFzf@uIlRwCbG_AhKaBvj@wIfh@gI|\\wF|OmCxKwAxM_ApUi@nAFhQx@pt@pG~@H|iA|JfMUlCg@jAe@tA}@vAwAlAaBb@eAt@uD`@oEVaHT{Fx@kNxAoHpA}CzByBjCcAjDa@ra@nBfd@rFbc@jF~^pGbKhDl\\tGrS~BrYPfSw@nPcAdFm@tg@oDdSYhPf@jS|Bld@vGly@lLvOrAbCNrMH`lFzHbSNlRNd[q@nMw@dK{@zLkBpJuBjLoCpJuCrJqD|IeF~QgLrNmI`NkHvPeGlNwDfUyEnRcDrKgA~Po@hQU|RMn~@JrPSdNi@pKq@xNyAtJsA`JaBxKoAhJcBjGwAvLcCbIaBbJqBrGeAjFq@~FU|DS~CQ|Hc@tCKdEEvGOpJAdNJ|K\\jI\\zJZbHt@xIpA~LlAxMbArLlBvLv@|I~@hK~AdJlBjLbAvLp@fL`A`HZjMdAlLIzMu@hN}@nKwAlO_CfNuCzQ}EdK}CfIeDjPsH~HaExJuFtG}D~PsMjP_OlPaUtJ{OfOk\\pIkRzNqY|]kf@nSmTrWmU`QqMtKyGtJiFdZaOl\\cP|b@qSfGuC~d@oSfYaNhh@mVr^_QbW_Mdt@y]fQiIxT{K~Au@nx@y^~IiFlHkFnPcOl]a[vFcDpGsBxLwB|HCjI^dQvDlMdHx_@pNtK|ApSIfSiF~rBaaAnKaFfP}HxoBgbAlkCukAbU_M|IqFbOiIhnAkt@p]mU~PcMzh@_d@zRqRbHaJvJeMfK{Mze@qf@hl@kh@zI_GzLwGbJyD~ImCtOsCbNgAjTBjXpC`TtFfM`Dh`@bM~ChAfK~CbGfBrHtBlJ|A~LrA`Np@dKZrsAr@lPJrLn@n^xCp]tEbv@fKjOdBp_AfLjOvAhNl@tSHdSe@bXqCpV_FhRsFjQgHrd@oSvb@cTxx@{a@~a@cTzR_L`G{Cr{BefAnl@}YlLgGpbCglAzR}J|Q}IrXcNfw@c`@tM_IrE{BpJgFnGeFjFeGdDaGtCqItA{Gz@wI^wGX{Gn@oGdC}HzDeG`GaFxFuCvFkCpR_HdIqDjHaEpFuEjDoEtD}G~BkFzAuFbAqDvE{CrEsStDiSzCgQhAePJiIIsIaBe]yK{sA{LwcBcRwdBgAqTuBg^gC_[_G_v@sAeP{F}TqC{R{Fua@sNcd@{UofAcBaHyCyJwDaFoEwCoGmAwEO}FRsIlBeMrDePnHwPbMgNlMgJlKyLbNiIxOaHjT{ElVyBxSo@j\\BnJKbVAtD?xAA|Ac@fZWxRo@jVo@vjB[~|A]nOcBdSyHpTiN|LwPnC}OUmTmEk[}LaOoFkScCsYlBgRxFyvAdk@", + "distanceM": 25062.2, + "durationS": 1678.2 + } + ] + }, + { + "pairId": "kl-sentral-to-sunway-pyramid", + "origin": { + "lat": 3.1343, + "lng": 101.6869, + "label": "KL Sentral" + }, + "destinationId": "sunway-pyramid", + "routes": [ + { + "geometry": "_zg~D{un}`E}NkZsFcKkCgBe@QaFyAkEa@wFBaF~AsHfFuf@hVuBlKJhDjA~C`BjExA`EnHfQvAfD|AtD`B`E~GhP|EhLxH|QxHxQ`AxAzA~@jBd@lBGhBIfB_@xEaAx@WnC}@jEqBtGw@~KqFvLaGzFsCtCwAbA[lGSrCAbBFvAThA\\lAv@~C|BtQhTtDt@|AUlBc@xa@o\\zI}HtA}Ax@mCB}ABeB_@sEwHyIkCyCUW}KkMxEuD`N_LlLwJzCiCbE`GfCr@dBGzGeCfVcMj@W~LsFtAo@tRtUzJhL~FdHbO`QxBpC|DlEt@z@jFbGbDpDjF`GpObQ|ExFnDnErBfChFtGnBbCpA|AvKvMhBlDtBvGLfDCdCCtBWfCi@jDqHlR}A`Ew@nBOfATpBh@jA`A~@~RhIfQpEnZnLbHdDz\\fSrCnA~I`ExMpFnGjCnJ|DhIfDhJ`DvDr@fEBdIi@fHqAtDcB`x@sl@lP{LtBsEp@eCr@qCFoDKsDaB_FuCoEeFcBsDu@sCYgEJiDr@oCnA_DtAmClG_AnEEtEd@rDjAtDlp@joAxRvZ`t@veAbKzKbLvJxPhJ|VlIzUzEf^hAfGK|LSxDIb\\s@zAEx@CpDCje@_@fa@[vFBn^|DdOjCxM|DrNrFfMlHzKdH~JvHfOdQrG~JtFhLjF`QbFpQbFnWtBxK~@nGnBdQ`Dxb@fC|\\zB`d@pD|cAXzr@Pjb@?nF{@|xAqAvx@UzMeB~fAsAhy@kA`i@k@`Tu@fc@eGpdDQdWZlXDfDBnKtBbVdB~MfCdNtHhXxHpTrHxQjI|PxLtVzO`\\ncA`uBpi@fhAnh@reAhLzSzMvU`o@dgAltAfbCpnAzgCjs@l~AhRfh@vNve@tNtj@bI|YbF~ObMr[vOh^lIpQtXhg@hY|e@pNzVhSfb@riAncC|^hx@tPlZjIhMjL`PnQpRpS|P~U~NlJ~EpQpHt^nLlg@fKzbBrYps@fPdq@jT~ZrKjObGlOjHjKpF|FtCr_@nTnMxIxt@rk@rk@`k@hb@vh@htAppBbLhQ|eDz|Ep_@po@vPt]pOj^~Rdl@`I~VxIl_@|Jjk@fFh`@`H`y@Z|E\\dFvA~S`NjuB~FpcAnAne@nCpLbCnWFdAr@dKrI`dAnAlE|@zApAbB~A~@hAZrAT~Ch@rAGlMi@|Qi@fSQt_@L`^U`U@`UXhbBu@lg@M|YI~UEzDeAbu@_@|bAd@`_@?~KTzMDdH|FbCzBrFxE|@fDIpJ@lUbAxQ|NhdBtGtm@|ApOp@bMPlHRbRDtDFjZDjJ`C|LTjLApH@hIAdDG~K]zL@xJRhIPxEh@nG`@nEdAvCrB|BhB|AfDxA|DHhM@", + "distanceM": 14168.6, + "durationS": 1002.7 + } + ] + }, + { + "pairId": "berjaya-times-square-to-titiwangsa", + "origin": { + "lat": 3.1421, + "lng": 101.7101, + "label": "Berjaya Times Square" + }, + "destinationId": "titiwangsa", + "routes": [ + { + "geometry": "g`y~Dkd{~`EJb@bEpQ`BvH|BjIuH`DmB\\Lf@lDlN`D|MzA~DrAxDtKdR~OlXt@dArT`[nRjZnC`EtDzEvArBxBbDXb@h@t@lA`BdCfDvXv\\|BjCpErExFpE`FpC|F|BpI`CxHnAzIt@fQ|B|EhAvFlBtJtErJfGpGhDvDvBtXpO~F`DzJjG`QnJhEtBnDtAvGnBfCt@`M`CpC^~P^aEpL[~@yF~OcAbEqCvIgLpg@Sz@uCpMMh@iElRgA~EcH`V{DvEmExPuDjKkIvPaIbMeYf]cPbRy]bd@kL|MkHbIeBrBiB|BkCfEeCrFkDrK}@|CiAhF_A~DgC`MkCrQa@xCyD~Y_DxR}Gfd@eItk@}Eh]gD|R{ApG_ClG}FlKyKnMgF|DgFfC_FlCkH|B_KfCsVxCkXzCsLh@kXM{Hc@yJoAcSqE}c@oK{JiBsOgBgJCcGG}DToAJiGx@}FbB}GfC_H~CaJrF}T|NsPzHgM|DsO`EsFbBiNhBoPp@kRBmZ{Bis@uJ_Y_DcYyC}b@gFik@mG_e@iGm[{EsYwEoXgFq_@gEes@uJ_F}@oEf@sO_LwQ{CcUcCqScB_JW_K@sId@}HhAyTdEsV`GgZdJg]~JwNxE_k@|O{^dKkWhHsPtEkN~DgDj@_QtCcHg@g\\pAup@rCiFIwBUuASsA_@eAs@iAu@}@y@_A_Ao@_Ag@iAk@eBg@uBSsBAuBL_C`@wBj@kBz@eBhAaB~A{AbBaAvBy@tEw@tEq@~Ce@bFsAdJmCbEwAxDaBhGiEjCqDpF_LhGsQpCeJbCoEdFoNjXuf@hDaG|BaEpA{B~CsFdKqQ~OgXrDkG|IgO|CsFt@sAvBaExJsQvE}L|C_LNg@tCmKh@eCh@cCl@kGl@iKPoK~Bg`@DcA|Cky@t@wKh@{GlA}J`BsL`GqYVaIEq@I}AyAqCc@a@gAuAwAoAiBmAeGaDoBc@_E_AuZ}GaAU}IqBeFkAkXkGkGwAoEeAcJsBmVwFcCk@aXgGuJyB{P}DkCm@wD}@gC[mAGsVIaFB_FCkQKqHEmGC_B?wDAyRAsPAmG?oA?qMAmSAcM?g@@]BeA?mHAqA?iFAwHAo@?_[EaDAyL?oFGkDDuF?wA?wA?wKAcD?uI@}KE_DAoECqHCmDAcAAcSe@cUg@{IMaNm@i@CqIe@eIe@sIe@qIc@iDQcG[gBImJg@eFUq@E}@Nk@T}@x@MtF[dPGfDGvAIxGAfAEtC?dEQ|H_Bbc@o@M{B_@{HsAmDIsKQ", + "distanceM": 7691.7, + "durationS": 651.2 + }, + { + "geometry": "g`y~Dkd{~`EJb@bEpQ`BvH|BjIuH`DmB\\Lf@lDlNhQdSjC`C|ClBrAXtBKxDJtD{AbFkCpNaGpEcEbHeEjIeE`DqBzDeF~AiCbFoLfCuJhBaP|AuRnByQDwDLyCT_F`@mI~Aa]nEsq@JwAJeBbAcOt@mLl@kLlDyWpAqMzIu{@?sB?uE{@uI_CiJKe@oDmH_JeKaGaEgN_CoF}AmG}A_DQyCI_DNmC`@cB`@wAt@sA`AaAdAeAdCi@rBY`CEnCR|Bl@vBbAxBnBpBtC`BlEr@vC@`COxBk@zCgBvDuDnCiEzGuMrKyRjFoKlEmRr@aRZ{CrBiMzT_lAtAoHhAcGpA_HdBeJ@sTiAcK_G}O_JgNyBe@_Gk@sASaRuCqFeAyDu@qOaFmGsBmJ}CaA[uNkF}JsD}IiDyOiG{iAi[iAYaNeDkAY}NqDgNaDmEcAyLgFwKmCwbBw\\a_A}Qcg@cK{TgDmQgDcW{@oQJigA`Ca_@jBwFJce@ByG@{M?}b@RuL^wXhC{BZwBb@aZbKqYnK{n@~T_LbEoVfIqVfIev@bU{FzAaMnCoE`Am[~G}`AvPmLjCg{C|_Ami@rUkDhCqQ|M{DfDiFxIEFgL|XyFjZuEjToDvMmDbMuD`JmEpI{G`KuGpHyFzFcFzDoFbD}StKqU`K}h@pWeOlG}N|HmR|JuKhGwGxEgHfGsGzGwc@`j@uc@nj@qfAptAeXx^{U|YqPvSqZt^aD~DwH~JwFbImIpMiJ|OqCnFwInQsBlEgMxZgCxGwExOsBfKeBrKoAdIaBbL?nFPtDl@~BdAnAjA`Ajd@rIdFpAfKbCdCn@bCl@zw@dVjDdAdBl@{BpCm@jAu@`AsBnC_BbBgGtGcA~Ds@tJKhBUlNUxKQzGMdIH~OE`ASfLKrCSnFMtF[dPGfDGvAIxGAfAEtC?dEQ|H_Bbc@o@M{B_@{HsAmDIsKQ", + "distanceM": 7168.6, + "durationS": 670.1 + } + ] + } + ] } diff --git a/routing/test/contract.test.mjs b/routing/test/contract.test.mjs index 6fc8062..b1ef829 100644 --- a/routing/test/contract.test.mjs +++ b/routing/test/contract.test.mjs @@ -141,9 +141,9 @@ test("C5 zero runtime dependencies — routing/package.json has no dependencies ); }); -/* ————— C6 — the committed seed's ODbL header (T-05-06) ————— */ +/* ————— C6 — the committed dataset's ODbL header (T-05-06) ————— */ -test("C6 ODbL header present — the committed seed carries license + attribution + notice", () => { +test("C6 ODbL header + populated corpus — the committed dataset carries license, attribution, notice, and the golden pairs", () => { const seed = JSON.parse( readFileSync(path.join(srcDir, "data", "route-fixtures.json"), "utf8"), ); @@ -155,9 +155,9 @@ test("C6 ODbL header present — the committed seed carries license + attributio assert.ok(String(seed._notice).includes("ODbL"), "the notice must name ODbL"); assert.equal(seed.version, 1); assert.ok(Array.isArray(seed.pairs), "pairs must be an array"); - assert.equal( - seed.pairs.length, - 0, - "the seed ships EMPTY — plan 05-02's build-time script populates it", + assert.ok( + seed.pairs.length >= 10 && seed.pairs.length <= 20, + `the committed corpus carries the golden pairs (10..20) — 05-01 shipped an EMPTY seed ` + + `explicitly so plan 05-02's build-time script could populate it; found ${seed.pairs.length}`, ); }); diff --git a/routing/test/fixtures/golden-pairs.json b/routing/test/fixtures/golden-pairs.json new file mode 100644 index 0000000..11a84ac --- /dev/null +++ b/routing/test/fixtures/golden-pairs.json @@ -0,0 +1,24 @@ +{ + "_comment": "Pinned golden-pair corpus (D-02/D-12). Origins AND destinations are gazetteer landmarks (public data). RT2b proves every origin equals a gazetteer coordinate. national-museum is never a destinationId (RT4) — it is the 05-05 pending-probe carrier.", + "klBbox": { "minLat": 2.9, "maxLat": 3.4, "minLng": 101.4, "maxLng": 102.0 }, + "fixtureGzipBudgetBytes": 102400, + "pendingProbeDestinationId": "national-museum", + "alternativesFloorPairs": 4, + "pairs": [ + { "pairId": "kl-tower-to-kl-sentral", "originId": "kl-tower", "origin": { "lat": 3.1528, "lng": 101.7038, "label": "Kuala Lumpur Tower" }, "destinationId": "kl-sentral" }, + { "pairId": "kl-sentral-to-suria-klcc", "originId": "kl-sentral", "origin": { "lat": 3.1343, "lng": 101.6869, "label": "KL Sentral" }, "destinationId": "suria-klcc" }, + { "pairId": "suria-klcc-to-bangsar", "originId": "suria-klcc", "origin": { "lat": 3.1578, "lng": 101.7117, "label": "Suria KLCC" }, "destinationId": "bangsar" }, + { "pairId": "mid-valley-to-bukit-bintang", "originId": "mid-valley", "origin": { "lat": 3.118, "lng": 101.6779, "label": "Mid Valley Megamall" }, "destinationId": "bukit-bintang" }, + { "pairId": "bukit-bintang-to-mid-valley", "originId": "bukit-bintang", "origin": { "lat": 3.1466, "lng": 101.7106, "label": "Bukit Bintang" }, "destinationId": "mid-valley" }, + { "pairId": "pwtc-to-sunway-velocity", "originId": "pwtc", "origin": { "lat": 3.1768, "lng": 101.698, "label": "PWTC" }, "destinationId": "sunway-velocity" }, + { "pairId": "masjid-jamek-to-lake-gardens", "originId": "masjid-jamek", "origin": { "lat": 3.1484, "lng": 101.6953, "label": "Masjid Jamek" }, "destinationId": "lake-gardens" }, + { "pairId": "titiwangsa-to-petaling-street", "originId": "titiwangsa", "origin": { "lat": 3.1714, "lng": 101.7016, "label": "Taman Tasik Titiwangsa" }, "destinationId": "petaling-street" }, + { "pairId": "sunway-pyramid-to-pavilion-kl", "originId": "sunway-pyramid", "origin": { "lat": 3.0726, "lng": 101.6069, "label": "Sunway Pyramid" }, "destinationId": "pavilion-kl" }, + { "pairId": "central-market-to-merdeka-118", "originId": "central-market", "origin": { "lat": 3.1454, "lng": 101.6953, "label": "Central Market" }, "destinationId": "merdeka-118" }, + { "pairId": "little-india-brickfields-to-aquaria-klcc", "originId": "little-india-brickfields", "origin": { "lat": 3.1306, "lng": 101.6869, "label": "Brickfields" }, "destinationId": "aquaria-klcc" }, + { "pairId": "national-museum-to-pudu-raya", "originId": "national-museum", "origin": { "lat": 3.1378, "lng": 101.687, "label": "National Museum" }, "destinationId": "pudu-raya" }, + { "pairId": "mid-valley-to-ioi-city-mall", "originId": "mid-valley", "origin": { "lat": 3.118, "lng": 101.6779, "label": "Mid Valley Megamall" }, "destinationId": "ioi-city-mall" }, + { "pairId": "kl-sentral-to-sunway-pyramid", "originId": "kl-sentral", "origin": { "lat": 3.1343, "lng": 101.6869, "label": "KL Sentral" }, "destinationId": "sunway-pyramid" }, + { "pairId": "berjaya-times-square-to-titiwangsa", "originId": "berjaya-times-square", "origin": { "lat": 3.1421, "lng": 101.7101, "label": "Berjaya Times Square" }, "destinationId": "titiwangsa" } + ] +}