@unmap/routing 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/index.d.ts +14 -7
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,8 +33,10 @@ and, when the body has one, `code`. An endpoint outside Canada is `422` `outside
|
|
|
33
33
|
A longitude or latitude outside WGS84 is `400` `invalid_location`, not that code.
|
|
34
34
|
A Canadian trip the graph cannot connect, including Iqaluit to the mainland, is `422` `no_route`.
|
|
35
35
|
|
|
36
|
-
Transit requires `departAt` and
|
|
37
|
-
`
|
|
36
|
+
Transit requires `departAt` and works on `route()` and `isochrone()`; a transit band can be a
|
|
37
|
+
`MultiPolygon`, and a centre with no scheduled transit nearby throws a `RouterError` with code
|
|
38
|
+
`outside_coverage`. Production routes the agencies `/coverage.json` lists as routing-ready. There is
|
|
39
|
+
no `arriveBy`, transit matrix, or realtime claim.
|
|
38
40
|
|
|
39
41
|
Parameters, response shapes, and error bodies are documented at
|
|
40
42
|
[unmap.dev/docs/apis/routing](https://unmap.dev/docs/apis/routing).
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface RouterOptions {
|
|
|
9
9
|
* Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.
|
|
10
10
|
*
|
|
11
11
|
* On by default. `Retry-After` is on a `429` other than `billing_required`, and on a container
|
|
12
|
-
* `503`. A `502` has none: routing
|
|
12
|
+
* `503`. A `502` has none: routing and `/elevation` both use
|
|
13
13
|
* `upstream_error`. With no header the client backs off itself. A wait longer than
|
|
14
14
|
* `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.
|
|
15
15
|
* See `RetryPolicy` in `@unmap/core`.
|
|
@@ -140,7 +140,7 @@ interface RoadRouteOptions extends SharedRouteOptions {
|
|
|
140
140
|
*/
|
|
141
141
|
useHills?: number;
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
143
|
+
/** Transit options are a separate branch so `departAt` cannot be omitted by accident. */
|
|
144
144
|
type RouteOptions = RoadRouteOptions | TransitRouteOptions;
|
|
145
145
|
/**
|
|
146
146
|
* How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a
|
|
@@ -488,15 +488,22 @@ interface FeatureCollection {
|
|
|
488
488
|
type: "FeatureCollection";
|
|
489
489
|
features: unknown[];
|
|
490
490
|
}
|
|
491
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Travel-time bands around a point. Transit bands ride scheduled transit from `departAt`, and a
|
|
493
|
+
* band can be a `MultiPolygon`: the area around the stop a ride drops you at is often its own
|
|
494
|
+
* island. A centre with no scheduled transit nearby is a `RouterError` with code
|
|
495
|
+
* `outside_coverage`, not a walking-only polygon.
|
|
496
|
+
*/
|
|
497
|
+
type IsochroneOptions = (RoadRouteOptions | TransitRouteOptions) & {
|
|
498
|
+
/** Travel-time bands in minutes. At most four, each 120 or less. */
|
|
492
499
|
minutes: number[];
|
|
493
|
-
}
|
|
500
|
+
};
|
|
494
501
|
/**
|
|
495
502
|
* A routing failure.
|
|
496
503
|
*
|
|
497
504
|
* Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside
|
|
498
|
-
* every other unmap client's errors. `
|
|
499
|
-
*
|
|
505
|
+
* every other unmap client's errors. `status` and `code` carry the HTTP status and the
|
|
506
|
+
* gateway's error code, and `retryAfterSeconds` comes from the `Retry-After` header.
|
|
500
507
|
*/
|
|
501
508
|
declare class RouterError extends UnmapError {
|
|
502
509
|
constructor(status: number, message: string, code?: string, retryAfterSeconds?: number);
|
|
@@ -509,7 +516,7 @@ declare class Router {
|
|
|
509
516
|
private readonly retry;
|
|
510
517
|
constructor(opts: RouterOptions);
|
|
511
518
|
route(from: LngLat, to: LngLat, opts?: RouteOptions): Promise<RouteResult>;
|
|
512
|
-
isochrone(
|
|
519
|
+
isochrone(near: LngLat, opts: IsochroneOptions): Promise<FeatureCollection>;
|
|
513
520
|
/**
|
|
514
521
|
* The routing graph's state: whether the engine is answering, which version it is, and how
|
|
515
522
|
* old the Canada graph is.
|
package/dist/index.js
CHANGED
|
@@ -92,21 +92,21 @@ var Router = class {
|
|
|
92
92
|
opts.signal
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
|
-
isochrone(
|
|
95
|
+
isochrone(near, opts) {
|
|
96
96
|
if (opts.avoidAreas != null) {
|
|
97
97
|
return this.post(
|
|
98
98
|
"/isochrone",
|
|
99
|
-
{
|
|
99
|
+
{ near, minutes: opts.minutes, ...bodyOptions(opts) },
|
|
100
100
|
opts.signal
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
return this.get(
|
|
104
104
|
"/isochrone",
|
|
105
105
|
{
|
|
106
|
-
|
|
107
|
-
lat: String(center[1]),
|
|
106
|
+
near: near.join(","),
|
|
108
107
|
minutes: opts.minutes.join(","),
|
|
109
108
|
mode: opts.mode,
|
|
109
|
+
depart_at: opts.departAt != null ? departureValue(opts.departAt) : void 0,
|
|
110
110
|
avoid: opts.avoid?.length ? opts.avoid.join(",") : void 0,
|
|
111
111
|
use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
|
|
112
112
|
use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.\n *\n * On by default. `Retry-After` is on a `429` other than `billing_required`, and on a container\n * `503`. A `502` has none: routing uses `routing_failed`, and `/elevation` uses\n * `upstream_error`. With no header the client backs off itself. A wait longer than\n * `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\n}\n\n/** `car` is the same costing as `auto`, spelled for people who do not think in Valhalla terms. */\nexport type TravelMode = \"auto\" | \"car\" | \"bicycle\" | \"pedestrian\" | \"truck\" | \"transit\";\nexport type RoadTravelMode = Exclude<TravelMode, \"transit\">;\n/** [longitude, latitude]. */\nexport type LngLat = [number, number];\n\n/**\n * Vehicle profile for `mode: \"truck\"`. Metric throughout: metres and tonnes. Every field is\n * optional; the gateway falls back to Valhalla's defaults (4.11 m high, 2.6 m wide, 21.64 m\n * long, 21.77 t, 9.07 t axle load, 5 axles, no hazmat) for anything omitted.\n */\nexport interface TruckProfile {\n /** Vehicle height in metres. */\n height?: number;\n /** Vehicle width in metres. */\n width?: number;\n /** Vehicle length in metres. */\n length?: number;\n /** Gross vehicle weight in tonnes. */\n weight?: number;\n /** Load per axle in tonnes (0–40). */\n axleLoad?: number;\n /** Number of axles (2–20). */\n axleCount?: number;\n /** Carrying hazardous materials: avoids roads tagged `hazmat=no`. */\n hazmat?: boolean;\n /** Preference for designated truck routes, 0 (none) to 1 (strong). */\n useTruckRoute?: number;\n}\n\n/**\n * A road feature to weight against. `tolls` and `highways` are driving modes only; `ferries`\n * applies to every road mode. `unpaved` is deliberately absent: Valhalla has no surface term in its\n * driving costing, so it could only ever have meant two different things by mode.\n */\nexport type AvoidToken = \"tolls\" | \"highways\" | \"ferries\";\n\n/** A GeoJSON position. The gateway requires exactly `[longitude, latitude]` at runtime. */\nexport type AvoidAreaPosition = number[];\n/** A closed GeoJSON linear ring. */\nexport type AvoidAreaRing = AvoidAreaPosition[];\n\n/** Polygon with one exterior ring. Interior rings are rejected by the gateway. */\nexport interface AvoidAreaPolygon {\n type: \"Polygon\";\n coordinates: AvoidAreaRing[];\n}\n\n/** Multiple polygons, each with one exterior ring. */\nexport interface AvoidAreaMultiPolygon {\n type: \"MultiPolygon\";\n coordinates: AvoidAreaRing[][];\n}\n\nexport interface AvoidAreaFeature {\n type: \"Feature\";\n geometry: AvoidAreaPolygon | AvoidAreaMultiPolygon;\n properties?: Record<string, unknown> | null;\n}\n\nexport interface AvoidAreaFeatureCollection {\n type: \"FeatureCollection\";\n features: AvoidAreaFeature[];\n}\n\nexport type AvoidAreas =\n | AvoidAreaPolygon\n | AvoidAreaMultiPolygon\n | AvoidAreaFeature\n | AvoidAreaFeatureCollection;\n\ninterface SharedRouteOptions {\n signal?: AbortSignal;\n}\n\nexport interface TransitRouteOptions extends SharedRouteOptions {\n mode: \"transit\";\n /**\n * Scheduled departure. A Date is serialized as an ISO instant; a\n * string is sent unchanged so callers can provide an ISO value with an explicit local offset.\n * Transit supports departure time only. There is no `arriveBy` option.\n */\n departAt: Date | string;\n avoid?: never;\n avoidAreas?: never;\n truck?: never;\n useHighways?: never;\n useHills?: never;\n}\n\nexport interface RoadRouteOptions extends SharedRouteOptions {\n mode?: RoadTravelMode;\n /** A scheduled departure applies only to `mode: \"transit\"`. */\n departAt?: never;\n /**\n * Road features to weight against.\n *\n * **A preference, not an exclusion.** These set Valhalla costing weights to 0, which\n * penalises the feature heavily but does not forbid it: a route whose only path crosses a\n * toll bridge still crosses it, and nothing in the result says that it did. If the\n * distinction matters to you, check the returned geometry. Avoid areas have a separate,\n * feature-gated API because they use Valhalla's exclusion mechanism rather than a weight.\n *\n * `tolls` and `highways` are driving modes only (`auto`, `car`, `truck`); `ferries` applies\n * to every road mode. `avoid: [\"highways\"]` and `useHighways` are the same knob at different\n * resolutions and the gateway rejects both together.\n */\n avoid?: AvoidToken[];\n /**\n * Areas to keep the route out of, as a GeoJSON `Polygon`, `MultiPolygon`, or\n * `FeatureCollection` of either.\n *\n * Valhalla defines these as exclusions rather than preferences. Server support remains behind\n * a feature flag while a defect in the pinned engine can snap a location across the polygon;\n * do not rely on this option until the service documentation says the flag is enabled.\n * Interior rings are rejected because the engine cannot preserve holes.\n *\n * At most 8 areas, 100 vertices and 10 km of perimeter in total.\n *\n * Sending this makes `route()` and `isochrone()` use POST instead of GET, because a polygon\n * cannot ride a query string. The response is identical either way.\n */\n avoidAreas?: AvoidAreas;\n /** Only meaningful with `mode: \"truck\"`; the gateway rejects it on any other mode. */\n truck?: TruckProfile;\n /**\n * Willingness to take motorways and trunks, 0 (avoid) to 1 (prefer). Driving\n * modes only (`auto`, `car`, `truck`). This is Valhalla's existing costing\n * option on the Canada graph, not a second router and not resource/winter/field routing.\n */\n useHighways?: number;\n /**\n * Willingness to climb, 0 (avoid hills even for a longer route) to 1 (indifferent).\n * `bicycle` and `pedestrian` only; the gateway rejects it on the driving modes, whose\n * costing has no grade term at all. Grades come from the national 30 m elevation model,\n * baked into the Canada graph at build time.\n */\n useHills?: number;\n}\n\n/** Route-only transit options are a separate branch so `departAt` cannot be omitted by accident. */\nexport type RouteOptions = RoadRouteOptions | TransitRouteOptions;\n\n/** `TruckProfile` field → the gateway's snake_case query parameter. */\nconst TRUCK_PARAMS: Record<keyof TruckProfile, string> = {\n height: \"height\",\n width: \"width\",\n length: \"length\",\n weight: \"weight\",\n axleLoad: \"axle_load\",\n axleCount: \"axle_count\",\n hazmat: \"hazmat\",\n useTruckRoute: \"use_truck_route\",\n};\n\nfunction truckParams(truck?: TruckProfile): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = String(v);\n }\n return out;\n}\n\n/** `TruckProfile` field to the gateway's snake_case body field, values left as numbers. */\nfunction truckBody(truck?: TruckProfile): Record<string, number | boolean> {\n const out: Record<string, number | boolean> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = v;\n }\n return out;\n}\n\n/**\n * The option fields both POST endpoints accept, shaped for a JSON body.\n *\n * Every option `RouteOptions` declares has to appear here. One left out still typechecks at the\n * call site and is then dropped on the floor, so the caller asks for something and silently does\n * not get it.\n */\nfunction bodyOptions(opts: RouteOptions): Record<string, unknown> {\n return {\n ...(opts.mode ? { mode: opts.mode } : {}),\n ...(opts.departAt != null ? { depart_at: departureValue(opts.departAt) } : {}),\n ...(opts.avoid?.length ? { avoid: opts.avoid } : {}),\n ...(opts.avoidAreas != null ? { avoid_areas: opts.avoidAreas } : {}),\n ...(opts.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\n}\n\nfunction departureValue(value: Date | string): string {\n return value instanceof Date ? value.toISOString() : value;\n}\n\n/**\n * How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a\n * grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description\n * of most of it. `not_modelled` means the routing engine cannot express the rule at all (a\n * conditional restriction); `not_checked` means nothing looked.\n */\nexport type TruckCoverage =\n | \"authoritative\"\n | \"osm_plus_authoritative\"\n | \"osm_only\"\n | \"not_modelled\"\n | \"not_checked\";\n\n/** How much of a truck route's restriction picture is trustworthy, overall. */\nexport type TruckVerification = \"authoritative\" | \"partial\" | \"osm_only\" | \"unknown\" | \"candidate_permit_route\";\n\nexport interface TruckWarning {\n /** Stable machine-readable code, e.g. `INCOMPLETE_VEHICLE_PROFILE`. Switch on this, not the text. */\n code: string;\n message: string;\n}\n\n/** Outcome of the endpoint check. `clear` means clear against what was checked, nothing more. */\nexport type TruckValidationStatus = \"clear\" | \"conflict\" | \"unknown\";\n\nexport interface TruckValidationFinding {\n /**\n * Where the finding applies. The two endpoints are the segments the routing engine does not\n * evaluate; `route` is anywhere along the path, which is where a time-limited restriction lands.\n */\n where: \"origin\" | \"destination\" | \"route\";\n code: string;\n message: string;\n /** The OSM way the finding concerns, when the engine named one. */\n wayId?: number;\n /** For a time-limited finding: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the upstream authority published them. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * What was independently checked after the route was produced.\n *\n * The routing engine does not apply restrictions to a route's first or last segment, because a\n * vehicle has to be able to leave where it is parked. This is the check that looks at those two\n * segments anyway.\n *\n * `conflicts` carries findings that are decided: a posted limit your declared vehicle exceeds, and\n * a full closure in force -- the road is shut whatever the vehicle. `unknowns` carries what could\n * not be decided, which includes a restriction whose kind cannot be compared against dimensions\n * at all (a time window, destination-only access), one whose parameter you did not declare, and\n * an endpoint the engine moved or one that sits on a road closed to trucks.\n *\n * An earlier release of this type said a dimension restriction could only ever be an unknown\n * because the engine did not expose the limit. That was wrong, and comparison against your\n * declared vehicle now happens; send a complete profile and you get real answers rather than\n * \"something is posted here\".\n *\n * `clear` means clear against `checkedRestrictionClasses`; it never means the route is legally\n * compliant.\n *\n * Codes worth branching on, beyond the endpoint ones:\n *\n * - `TEMPORARY_LIMIT_EXCEEDED` (conflict) a time-limited posted limit, in force now, that your\n * declared vehicle exceeds.\n * - `TEMPORARY_LIMIT_SCHEDULED` (unknown) the same for a window that has not started.\n * - `TEMPORARY_LIMIT_DEPENDS_ON_CONFIGURATION` (unknown) a structure posting different weights by\n * vehicle configuration, where your declared mass is over the strictest and within the most\n * permissive. Alberta posts 25 t for a single truck and 63.5 t for a B-train at one bridge, so a\n * declared 40 t is legal as one and illegal as the other. This service does not collect your\n * configuration, so it reports the ambiguity rather than picking one.\n * - `TEMPORARY_LIMIT_UNDECLARED` (unknown) a limit is posted and the parameter it constrains was\n * not declared.\n * - `ENDPOINT_ON_TRUCK_PROHIBITED_ROAD` (unknown) the road at an endpoint is closed to trucks, so\n * the route begins or ends on a different one.\n *\n * Read the message as well as the code on a temporary limit. Some are published by the authority\n * in a structured field and some are read out of its own written description; the message says\n * which, because they do not deserve equal confidence.\n *\n * Check `checkedRestrictionClasses` before trusting a `clear`. When the time-limited layer is\n * unavailable you get `TEMPORAL_RESTRICTIONS_NOT_CHECKED` in `unknowns`, which is a different\n * answer from \"no closures found\".\n */\nexport interface TruckValidation {\n status: TruckValidationStatus;\n /** Exactly what was examined, so you can tell what \"clear\" covered. */\n checkedRestrictionClasses: string[];\n conflicts: TruckValidationFinding[];\n unknowns: TruckValidationFinding[];\n}\n\n/**\n * What a truck route considered, and what it did not. Present on `mode: \"truck\"` results only.\n *\n * Read `warnings` before relying on a route. In particular `ENDPOINT_RESTRICTIONS_UNCHECKED`\n * appears whenever the endpoint check did not clear the route's first and last road segment,\n * which includes every route where the check could not run: restrictions are not applied to\n * those two segments, so clearance and load at the origin and destination must be verified\n * separately. Nothing here ever reports a road as unrestricted; the absence of a restriction\n * means none is known.\n */\nexport interface TruckMeta {\n /** The profile as sent, in the units sent. Never back-filled with the engine's defaults. */\n profile: TruckProfile;\n /** True only when height, width, length, weight and axle load were all supplied. */\n profileComplete: boolean;\n /**\n * Every comparable restriction posted on a road this route actually traversed, whether or not\n * your declared vehicle exceeds it.\n *\n * A statement about the route rather than a list of problems: a 20 t bridge on the path is\n * listed even for a 10 t vehicle, because knowing the route crosses it is worth having. The\n * ones your vehicle does exceed also appear in `validation.conflicts`.\n *\n * `value` is in the engine's unit for that type: metres for `max_height`, `max_width` and\n * `max_length`, tonnes for `max_weight` and `max_axle_load`.\n *\n * Absent rather than `[]` whenever the along-route check did not complete — a route with more\n * distinct roads than one pass can sample, a multi-leg trip, or an engine call that failed.\n * An empty array would say \"nothing is posted on this route\", which is a claim; absent says\n * nothing was established. Check `validation.checkedRestrictionClasses` for\n * `route_static_restrictions` to tell the two apart.\n */\n restrictions_applied?: { type: string; value: number; wayId: number }[];\n coverage: Record<string, TruckCoverage>;\n verification: TruckVerification;\n warnings: TruckWarning[];\n /** The endpoint check. Absent when it did not run. */\n validation?: TruckValidation;\n /**\n * Subdivision codes whose authoritative restrictions are in the deployed graph. A statement\n * about the graph, not about this route: no jurisdiction lookup is performed.\n */\n enrichedJurisdictions?: string[];\n}\n\n/**\n * Something about the route itself that you should know before using it. Every mode.\n *\n * Two things today, both worth reading.\n *\n * The engine moves a requested point to the nearest road it can use, and if there is no road near\n * your point it moves it a long way and still reports success. Asked for a route between two\n * places with no road between them, it answered `Ok` with a confident 10.1 km route having\n * relocated the destination 292.3 km. A route is still returned, because it is usually a good\n * route that merely ends short, but `ENDPOINT_RELOCATED` tells you it does not reach the place you\n * named. This matters most where it is least visible: a well pad, a remote lease, a mine, a new\n * subdivision.\n *\n * And a provincial authority may have a road on your path shut. The graph is rebuilt weekly and\n * cannot carry a closure, so `CLOSURE_IN_FORCE_NEAR_ROUTE` is how you learn that the route may go\n * through one; `CLOSURE_SCHEDULED_NEAR_ROUTE` is one that starts later, with `effectiveFrom`\n * saying when. Reported on every mode, because a shut road is shut to a car too.\n *\n * Branch on `code`, which is stable; the text may be reworded.\n */\nexport interface RouteWarning {\n /** Stable machine-readable code, e.g. `ENDPOINT_RELOCATED`. Switch on this, not the text. */\n code: string;\n message: string;\n /**\n * What it concerns: one of the two endpoints, or `\"route\"` for anything along the path, which\n * is where a closure lands.\n */\n where?: \"origin\" | \"destination\" | \"route\";\n /** For a time-limited warning: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the authority published them. Either end may be absent. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * The state of the routing graph behind every other call here.\n *\n * `graph` is absent when the engine did not report a build time. That is a real state, not an\n * error: the deployed Canada graph was built before the service was configured to report one.\n * Treat its absence as \"unknown\", never as \"fresh\". `graph.osmTimestamp` is absent until a\n * graph built by the current pipeline is deployed; treat that the same way.\n */\nexport interface RoutingStatus {\n /** The only graph there is. Canada-only is an architectural constraint, not a rollout stage. */\n region: \"canada\";\n /** `unavailable` means the routing container did not answer, not that this call failed. */\n status: \"ready\" | \"unavailable\";\n engine: { name: \"valhalla\"; version?: string };\n graph?: {\n /** When the tileset was last written, ISO 8601. */\n builtAt: string;\n /** Whole hours since `builtAt`. */\n ageHours: number;\n /**\n * When the source OSM extract was current, ISO 8601. Taken from the PBF header\n * (`osmosis_replication_timestamp`) at graph build time. Absent means unknown.\n */\n osmTimestamp?: string;\n };\n /**\n * How fresh each live restriction feed is. The other half of \"is routing current\".\n *\n * The graph is rebuilt weekly and cannot carry a closure, so closures and temporary limits come\n * from feeds refreshed every few minutes instead. A stale feed is why a truck route reports the\n * temporal layer unchecked, and this is where you can see it without inspecting a route.\n *\n * `fresh` means a route would trust it right now. `stale` means it exists but is too old to be\n * trusted, so routes report the layer unchecked. `absent` means no snapshot has ever been\n * written — for a keyed feed that usually means the key is not configured, which is a different\n * thing from decay. `ageSeconds` is omitted for `absent`, because there is nothing to age.\n *\n * Absent from the response entirely when the deployment has no feed store bound.\n */\n restrictionFeeds?: {\n /** Subdivision code, e.g. `CA-ON`. */\n jurisdiction: string;\n state: \"fresh\" | \"stale\" | \"absent\";\n ageSeconds?: number;\n }[];\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n /**\n * Route-level warnings, on every mode. Absent when there are none, so a plain route body is\n * unchanged. See `RouteWarning`. Two kinds exist today:\n *\n * - the route does not actually begin or end where you asked (`ENDPOINT_RELOCATED`);\n * - a road closure published by a provincial authority lies within 250 m of the path\n * (`CLOSURE_IN_FORCE_NEAR_ROUTE`, `CLOSURE_SCHEDULED_NEAR_ROUTE`).\n *\n * Closures are reported on every mode, not only on trucks: a washed-out highway is shut to a\n * car as well, and the graph is rebuilt weekly so it cannot carry one. Coverage is Ontario,\n * Alberta and British Columbia, which is where authoritative feeds exist; elsewhere no closure\n * warning means nothing was checked rather than nothing is closed.\n *\n * On `mode: \"truck\"` the same findings also appear inside `truck.validation`, which is a\n * self-contained compliance report and is meant to be readable on its own. The duplication is\n * deliberate: `warnings` is the answer to \"is anything wrong with this route\" for every caller\n * regardless of mode, and putting it somewhere different for trucks would be worse.\n */\n warnings?: RouteWarning[];\n /**\n * Truck restriction coverage and warnings. Present on `mode: \"truck\"` only, and a sibling of\n * the fields above rather than a wrapper around them, so nothing that reads a route today\n * changes. The gateway sends it already; this type ships with the next release.\n *\n * `mode: \"truck\"` itself is included with normal routing on every plan, and applies Valhalla\n * truck costing, the truck restrictions carried in the road network, and the vehicle profile\n * you send. The **Truck Intelligence** add-on gates the layer on top: authoritative Canadian\n * government restriction data, validation against your declared vehicle, and the coverage and\n * provenance reporting.\n *\n * Without the add-on this object is still here and says so. Every `coverage` class reads\n * `not_checked`, `verification` is `unknown`, `validation` is absent rather than an empty\n * report, and `warnings` carries `TRUCK_INTELLIGENCE_NOT_ENABLED`. `not_checked` rather than\n * `osm_only` is deliberate: `osm_only` is a claim about provenance, and without the add-on\n * nothing looked at all.\n *\n * `/route` never refuses over this. Closure warnings in `warnings` above are not gated either,\n * on any mode.\n */\n truck?: TruckMeta;\n /** Present on a multimodal result. Static GTFS is `scheduled`; realtime is a later phase. */\n scheduleType?: \"scheduled\" | \"realtime\";\n /** Walking access/egress and transit rides, in travel order. Present only for transit routes. */\n legs?: RouteLeg[];\n}\n\nexport interface TransitAgencySummary {\n id: string;\n name: string;\n url?: string;\n}\n\nexport interface TransitRouteSummary {\n id: string;\n shortName?: string;\n longName?: string;\n /** CSS hex colour, including `#`. */\n color?: string;\n /** CSS hex text colour, including `#`. */\n textColor?: string;\n}\n\nexport interface TransitStopTime {\n id: string;\n name: string;\n coordinates?: LngLat;\n arrival?: string;\n departure?: string;\n}\n\nexport interface PedestrianRouteLeg {\n mode: \"pedestrian\";\n durationSeconds: number;\n distanceMeters: number;\n}\n\nexport interface TransitRouteLeg {\n mode: \"transit\";\n durationSeconds: number;\n distanceMeters: number;\n agency?: TransitAgencySummary;\n route?: TransitRouteSummary;\n headsign?: string;\n departure?: string;\n arrival?: string;\n stops: TransitStopTime[];\n}\n\nexport type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * **`null` is a real value here, so handle it on every cell.** A pair the engine could not\n * connect is nulled in place and the rest of the grid still answers; the null is never dropped\n * and never shortens a row, so `durations[i][j]` is always `origins[i]` to `destinations[j]`.\n * A `0` is a real answer, which is what the diagonal returns.\n *\n * Treat a `null` as \"no answer for this pair\", never as proof that no road connects the two\n * points. Across 40 pairs probed on the deployed graph, every null was a pair `/route` answers.\n * An earlier version of this comment claimed `null` never appears; that was retracted\n * on 2026-09-20.\n *\n * `matrix()` rejects with a `RouterError` (status 400, code `invalid_location`) when a coordinate\n * is outside WGS84, and with status 422, code `outside_coverage`, when a location is outside\n * Canada, before the engine runs. It rejects with code `no_route` only for failures the\n * engine raises before computing anything: a location with no road near it, a set of locations\n * the road network does not connect (a property of the set, so no one coordinate is at fault),\n * or two locations further apart in a straight line than its matrix limit.\n */\nexport interface MatrixResult {\n /** Seconds. Rows are origins, columns are destinations. */\n durations: (number | null)[][];\n /** Metres, same indexing. */\n distances: (number | null)[][];\n}\n\n/**\n * A route through a set of stops, plus the order to visit them in.\n *\n * A route with one extra field rather than a shape of its own, the same reasoning that makes\n * `match()` return `RouteResult` outright: an optimized route is a route.\n */\nexport interface OptimizedRouteResult extends RouteResult {\n /** Indices into the request's `stops`, in visiting order. Always a permutation of them. */\n order: number[];\n}\n\n/** Minimal GeoJSON FeatureCollection (isochrone bands). */\nexport interface FeatureCollection {\n type: \"FeatureCollection\";\n features: unknown[];\n}\n\nexport interface IsochroneOptions extends RoadRouteOptions {\n minutes: number[];\n}\n\n/**\n * A routing failure.\n *\n * Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside\n * every other unmap client's errors. `instanceof RouterError` keeps working, and `status` and `code`\n * are still where they were; `retryAfterSeconds` is new and comes from the header.\n */\nexport class RouterError extends UnmapError {\n constructor(status: number, message: string, code?: string, retryAfterSeconds?: number) {\n super(status, message, code, retryAfterSeconds);\n this.name = \"RouterError\";\n }\n}\n\n/**\n * The gateway answers every error with a JSON body whose `error` says what went wrong\n * (\"mode must be one of auto, car, bicycle, pedestrian, truck, transit\", \"q required\"). Put that in the\n * message so a caller who logs the thrown error reads the reason, not just the status.\n */\nasync function errorFrom(res: { status: number; headers?: Headers; json(): Promise<unknown> }, path: string): Promise<RouterError> {\n const fallback = `route ${res.status} on ${path}`;\n let body: { error?: unknown; code?: unknown } | undefined;\n try {\n body = (await res.json()) as { error?: unknown; code?: unknown };\n } catch {\n body = undefined;\n }\n const reason = typeof body?.error === \"string\" ? body.error : undefined;\n const code = typeof body?.code === \"string\" ? body.code : undefined;\n const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);\n}\n\n/** Typed client for the unmap routing API (gateway `/route`, `/isochrone`, `/matrix`, `/match`, `/optimized-route`). */\nexport class Router {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n private readonly retry: RetryPolicy | false | undefined;\n\n constructor(opts: RouterOptions) {\n this.retry = opts.retry;\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Router as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n route(from: LngLat, to: LngLat, opts: RouteOptions = {}): Promise<RouteResult> {\n // A polygon cannot ride a query string, so an `avoidAreas` call switches verb. Not a\n // behaviour change the caller has to think about: same parameters, same response, and the\n // GET stays the default so a plain route keeps its cacheable URL.\n if (opts.avoidAreas != null) {\n return this.post<RouteResult>(\"/route\", { from, to, ...bodyOptions(opts) }, opts.signal);\n }\n return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n isochrone(center: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n if (opts.avoidAreas != null) {\n return this.post<FeatureCollection>(\n \"/isochrone\",\n { center, minutes: opts.minutes, ...bodyOptions(opts) },\n opts.signal,\n );\n }\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n lon: String(center[0]),\n lat: String(center[1]),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n /**\n * The routing graph's state: whether the engine is answering, which version it is, and how\n * old the Canada graph is.\n *\n * Resolves rather than rejects when routing is down: it answers `status: \"unavailable\"`,\n * because a status call that throws when the subject is unhealthy tells you nothing a failed\n * `route()` had not already told you.\n */\n status(opts: { signal?: AbortSignal } = {}): Promise<RoutingStatus> {\n return this.get<RoutingStatus>(\"/routing/status\", {}, opts.signal);\n }\n\n /**\n * Travel times and distances between every origin and every destination.\n *\n * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get\n * where an origin is also a destination, and a `null` is a real answer too: a pair the engine\n * could not connect is nulled in place while the rest of the grid answers. A coordinate outside\n * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside\n * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise\n * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a\n * set of locations the road network does not connect, or two locations past its straight-line\n * matrix limit.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RoadRouteOptions = {}): Promise<MatrixResult> {\n return this.post<MatrixResult>(\"/matrix\", { origins, destinations, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.\n *\n * Returns the same `RouteResult` `route()` returns: a matched trace is a route. A coordinate\n * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada\n * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.\n */\n match(coordinates: LngLat[], opts: RoadRouteOptions = {}): Promise<RouteResult> {\n return this.post<RouteResult>(\"/match\", { coordinates, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Order a set of stops and return the route through them. At least 3 and at most 10 stops.\n *\n * The first stop is the fixed start and the last is the fixed end; only the stops between\n * them are reordered. There is no round-trip or open-route toggle: that is a real constraint\n * of the underlying engine, not an omission here.\n *\n * Optimizing solves a cost matrix over every stop, but it does not inherit `matrix()`'s per-cell\n * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84\n * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects\n * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with\n * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network\n * connects, or two are further apart than the engine's straight-line matrix limit. There is no\n * partial tour either way, because a tour needs every leg.\n */\n optimize(stops: LngLat[], opts: RoadRouteOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n private async post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n const res = await fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n\n private async get<T>(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n { signal, headers: { accept: \"application/json\" } },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n\n// The shared error type and retry policy, re-exported so a caller who installed only this\n// package can `catch (e) { if (e instanceof UnmapError) }` without adding @unmap/core.\nexport { UnmapError, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n"],"mappings":";AAAA,SAAS,iBAAiB,YAAY,gBAAgB,uBAAyC;AAqwB/F,SAAS,cAAAA,aAAY,mBAAAC,wBAAyC;AAnmB9D,IAAM,eAAmD;AAAA,EACvD,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,eAAe;AACjB;AAEA,SAAS,YAAY,OAA0D;AAC7E,QAAM,MAA0C,CAAC;AACjD,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAqC;AAC3F,UAAM,IAAI,MAAM,KAAK;AACrB,QAAI,KAAK,KAAM,KAAI,KAAK,IAAI,OAAO,CAAC;AAAA,EACtC;AACA,SAAO;AACT;AAGA,SAAS,UAAU,OAAwD;AACzE,QAAM,MAAwC,CAAC;AAC/C,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAqC;AAC3F,UAAM,IAAI,MAAM,KAAK;AACrB,QAAI,KAAK,KAAM,KAAI,KAAK,IAAI;AAAA,EAC9B;AACA,SAAO;AACT;AASA,SAAS,YAAY,MAA6C;AAChE,SAAO;AAAA,IACL,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,IACvC,GAAI,KAAK,YAAY,OAAO,EAAE,WAAW,eAAe,KAAK,QAAQ,EAAE,IAAI,CAAC;AAAA,IAC5E,GAAI,KAAK,OAAO,SAAS,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IAClD,GAAI,KAAK,cAAc,OAAO,EAAE,aAAa,KAAK,WAAW,IAAI,CAAC;AAAA,IAClE,GAAI,KAAK,eAAe,OAAO,EAAE,cAAc,KAAK,YAAY,IAAI,CAAC;AAAA,IACrE,GAAI,KAAK,YAAY,OAAO,EAAE,WAAW,KAAK,SAAS,IAAI,CAAC;AAAA,IAC5D,GAAG,UAAU,KAAK,KAAK;AAAA,EACzB;AACF;AAEA,SAAS,eAAe,OAA8B;AACpD,SAAO,iBAAiB,OAAO,MAAM,YAAY,IAAI;AACvD;AAqXO,IAAM,cAAN,cAA0B,WAAW;AAAA,EAC1C,YAAY,QAAgB,SAAiB,MAAe,mBAA4B;AACtF,UAAM,QAAQ,SAAS,MAAM,iBAAiB;AAC9C,SAAK,OAAO;AAAA,EACd;AACF;AAOA,eAAe,UAAU,KAAsE,MAAoC;AACjI,QAAM,WAAW,SAAS,IAAI,MAAM,OAAO,IAAI;AAC/C,MAAI;AACJ,MAAI;AACF,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,SAAS,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ;AAC9D,QAAM,OAAO,OAAO,MAAM,SAAS,WAAW,KAAK,OAAO;AAC1D,QAAM,aAAa,gBAAgB,IAAI,SAAS,IAAI,aAAa,KAAK,IAAI,KAAK;AAC/E,SAAO,IAAI,YAAY,IAAI,QAAQ,SAAS,GAAG,MAAM,KAAK,QAAQ,MAAM,UAAU,MAAM,UAAU;AACpG;AAGO,IAAM,SAAN,MAAa;AAAA,EACD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,MAAqB;AAC/B,SAAK,QAAQ,KAAK;AAClB,SAAK,MAAM,KAAK;AAChB,SAAK,QAAQ,KAAK,WAAW,iBAAiB,QAAQ,QAAQ,EAAE;AAIhE,SAAK,YAAY,KAAK,SAAS,WAAW,MAAM,KAAK,UAAU;AAAA,EACjE;AAAA,EAEA,MAAM,MAAc,IAAY,OAAqB,CAAC,GAAyB;AAI7E,QAAI,KAAK,cAAc,MAAM;AAC3B,aAAO,KAAK,KAAkB,UAAU,EAAE,MAAM,IAAI,GAAG,YAAY,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,IACzF;AACA,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,QACE,MAAM,KAAK,KAAK,GAAG;AAAA,QACnB,IAAI,GAAG,KAAK,GAAG;AAAA,QACf,MAAM,KAAK;AAAA,QACX,WAAW,KAAK,YAAY,OAAO,eAAe,KAAK,QAAQ,IAAI;AAAA,QACnE,OAAO,KAAK,OAAO,SAAS,KAAK,MAAM,KAAK,GAAG,IAAI;AAAA,QACnD,cAAc,KAAK,eAAe,OAAO,OAAO,KAAK,WAAW,IAAI;AAAA,QACpE,WAAW,KAAK,YAAY,OAAO,OAAO,KAAK,QAAQ,IAAI;AAAA,QAC3D,GAAG,YAAY,KAAK,KAAK;AAAA,MAC3B;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,UAAU,QAAgB,MAAoD;AAC5E,QAAI,KAAK,cAAc,MAAM;AAC3B,aAAO,KAAK;AAAA,QACV;AAAA,QACA,EAAE,QAAQ,SAAS,KAAK,SAAS,GAAG,YAAY,IAAI,EAAE;AAAA,QACtD,KAAK;AAAA,MACP;AAAA,IACF;AACA,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,QACE,KAAK,OAAO,OAAO,CAAC,CAAC;AAAA,QACrB,KAAK,OAAO,OAAO,CAAC,CAAC;AAAA,QACrB,SAAS,KAAK,QAAQ,KAAK,GAAG;AAAA,QAC9B,MAAM,KAAK;AAAA,QACX,OAAO,KAAK,OAAO,SAAS,KAAK,MAAM,KAAK,GAAG,IAAI;AAAA,QACnD,cAAc,KAAK,eAAe,OAAO,OAAO,KAAK,WAAW,IAAI;AAAA,QACpE,WAAW,KAAK,YAAY,OAAO,OAAO,KAAK,QAAQ,IAAI;AAAA,QAC3D,GAAG,YAAY,KAAK,KAAK;AAAA,MAC3B;AAAA,MACA,KAAK;AAAA,IACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OAAO,OAAiC,CAAC,GAA2B;AAClE,WAAO,KAAK,IAAmB,mBAAmB,CAAC,GAAG,KAAK,MAAM;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,OAAO,SAAmB,cAAwB,OAAyB,CAAC,GAA0B;AACpG,WAAO,KAAK,KAAmB,WAAW,EAAE,SAAS,cAAc,GAAG,YAAY,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,EACxG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAuB,OAAyB,CAAC,GAAyB;AAC9E,WAAO,KAAK,KAAkB,UAAU,EAAE,aAAa,GAAG,YAAY,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,EAC5F;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,SAAS,OAAiB,OAAyB,CAAC,GAAkC;AACpF,WAAO,KAAK,KAA2B,oBAAoB,EAAE,OAAO,GAAG,YAAY,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,EACzG;AAAA,EAEA,MAAc,KAAQ,MAAc,MAAe,QAAkC;AACnF,UAAM,IAAI,IAAI,gBAAgB,EAAE,KAAK,KAAK,IAAI,CAAC;AAC/C,UAAM,MAAM,MAAM;AAAA,MAChB,KAAK;AAAA,MACL,GAAG,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC;AAAA,MACxB;AAAA,QACE,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,EAAE,QAAQ,oBAAoB,gBAAgB,mBAAmB;AAAA,QAC1E,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B;AAAA,MACA,KAAK;AAAA,IACP;AACA,QAAI,CAAC,IAAI,GAAI,OAAM,MAAM,UAAU,KAAK,IAAI;AAC5C,WAAO,IAAI,KAAK;AAAA,EAClB;AAAA,EAEA,MAAc,IACZ,MACA,QACA,QACY;AACZ,UAAM,IAAI,IAAI,gBAAgB,EAAE,KAAK,KAAK,IAAI,CAAC;AAC/C,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,EAAG,KAAI,KAAK,QAAQ,MAAM,GAAI,GAAE,IAAI,GAAG,CAAC;AAClF,UAAM,MAAM,MAAM;AAAA,MAChB,KAAK;AAAA,MACL,GAAG,KAAK,IAAI,GAAG,IAAI,IAAI,CAAC;AAAA,MACxB,EAAE,QAAQ,SAAS,EAAE,QAAQ,mBAAmB,EAAE;AAAA,MAClD,KAAK;AAAA,IACP;AACA,QAAI,CAAC,IAAI,GAAI,OAAM,MAAM,UAAU,KAAK,IAAI;AAC5C,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;","names":["UnmapError","parseRetryAfter"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.\n *\n * On by default. `Retry-After` is on a `429` other than `billing_required`, and on a container\n * `503`. A `502` has none: routing and `/elevation` both use\n * `upstream_error`. With no header the client backs off itself. A wait longer than\n * `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\n}\n\n/** `car` is the same costing as `auto`, spelled for people who do not think in Valhalla terms. */\nexport type TravelMode = \"auto\" | \"car\" | \"bicycle\" | \"pedestrian\" | \"truck\" | \"transit\";\nexport type RoadTravelMode = Exclude<TravelMode, \"transit\">;\n/** [longitude, latitude]. */\nexport type LngLat = [number, number];\n\n/**\n * Vehicle profile for `mode: \"truck\"`. Metric throughout: metres and tonnes. Every field is\n * optional; the gateway falls back to Valhalla's defaults (4.11 m high, 2.6 m wide, 21.64 m\n * long, 21.77 t, 9.07 t axle load, 5 axles, no hazmat) for anything omitted.\n */\nexport interface TruckProfile {\n /** Vehicle height in metres. */\n height?: number;\n /** Vehicle width in metres. */\n width?: number;\n /** Vehicle length in metres. */\n length?: number;\n /** Gross vehicle weight in tonnes. */\n weight?: number;\n /** Load per axle in tonnes (0–40). */\n axleLoad?: number;\n /** Number of axles (2–20). */\n axleCount?: number;\n /** Carrying hazardous materials: avoids roads tagged `hazmat=no`. */\n hazmat?: boolean;\n /** Preference for designated truck routes, 0 (none) to 1 (strong). */\n useTruckRoute?: number;\n}\n\n/**\n * A road feature to weight against. `tolls` and `highways` are driving modes only; `ferries`\n * applies to every road mode. `unpaved` is deliberately absent: Valhalla has no surface term in its\n * driving costing, so it could only ever have meant two different things by mode.\n */\nexport type AvoidToken = \"tolls\" | \"highways\" | \"ferries\";\n\n/** A GeoJSON position. The gateway requires exactly `[longitude, latitude]` at runtime. */\nexport type AvoidAreaPosition = number[];\n/** A closed GeoJSON linear ring. */\nexport type AvoidAreaRing = AvoidAreaPosition[];\n\n/** Polygon with one exterior ring. Interior rings are rejected by the gateway. */\nexport interface AvoidAreaPolygon {\n type: \"Polygon\";\n coordinates: AvoidAreaRing[];\n}\n\n/** Multiple polygons, each with one exterior ring. */\nexport interface AvoidAreaMultiPolygon {\n type: \"MultiPolygon\";\n coordinates: AvoidAreaRing[][];\n}\n\nexport interface AvoidAreaFeature {\n type: \"Feature\";\n geometry: AvoidAreaPolygon | AvoidAreaMultiPolygon;\n properties?: Record<string, unknown> | null;\n}\n\nexport interface AvoidAreaFeatureCollection {\n type: \"FeatureCollection\";\n features: AvoidAreaFeature[];\n}\n\nexport type AvoidAreas =\n | AvoidAreaPolygon\n | AvoidAreaMultiPolygon\n | AvoidAreaFeature\n | AvoidAreaFeatureCollection;\n\ninterface SharedRouteOptions {\n signal?: AbortSignal;\n}\n\nexport interface TransitRouteOptions extends SharedRouteOptions {\n mode: \"transit\";\n /**\n * Scheduled departure. A Date is serialized as an ISO instant; a\n * string is sent unchanged so callers can provide an ISO value with an explicit local offset.\n * Transit supports departure time only. There is no `arriveBy` option.\n */\n departAt: Date | string;\n avoid?: never;\n avoidAreas?: never;\n truck?: never;\n useHighways?: never;\n useHills?: never;\n}\n\nexport interface RoadRouteOptions extends SharedRouteOptions {\n mode?: RoadTravelMode;\n /** A scheduled departure applies only to `mode: \"transit\"`. */\n departAt?: never;\n /**\n * Road features to weight against.\n *\n * **A preference, not an exclusion.** These set Valhalla costing weights to 0, which\n * penalises the feature heavily but does not forbid it: a route whose only path crosses a\n * toll bridge still crosses it, and nothing in the result says that it did. If the\n * distinction matters to you, check the returned geometry. Avoid areas have a separate,\n * feature-gated API because they use Valhalla's exclusion mechanism rather than a weight.\n *\n * `tolls` and `highways` are driving modes only (`auto`, `car`, `truck`); `ferries` applies\n * to every road mode. `avoid: [\"highways\"]` and `useHighways` are the same knob at different\n * resolutions and the gateway rejects both together.\n */\n avoid?: AvoidToken[];\n /**\n * Areas to keep the route out of, as a GeoJSON `Polygon`, `MultiPolygon`, or\n * `FeatureCollection` of either.\n *\n * Valhalla defines these as exclusions rather than preferences. Server support remains behind\n * a feature flag while a defect in the pinned engine can snap a location across the polygon;\n * do not rely on this option until the service documentation says the flag is enabled.\n * Interior rings are rejected because the engine cannot preserve holes.\n *\n * At most 8 areas, 100 vertices and 10 km of perimeter in total.\n *\n * Sending this makes `route()` and `isochrone()` use POST instead of GET, because a polygon\n * cannot ride a query string. The response is identical either way.\n */\n avoidAreas?: AvoidAreas;\n /** Only meaningful with `mode: \"truck\"`; the gateway rejects it on any other mode. */\n truck?: TruckProfile;\n /**\n * Willingness to take motorways and trunks, 0 (avoid) to 1 (prefer). Driving\n * modes only (`auto`, `car`, `truck`). This is Valhalla's existing costing\n * option on the Canada graph, not a second router and not resource/winter/field routing.\n */\n useHighways?: number;\n /**\n * Willingness to climb, 0 (avoid hills even for a longer route) to 1 (indifferent).\n * `bicycle` and `pedestrian` only; the gateway rejects it on the driving modes, whose\n * costing has no grade term at all. Grades come from the national 30 m elevation model,\n * baked into the Canada graph at build time.\n */\n useHills?: number;\n}\n\n/** Transit options are a separate branch so `departAt` cannot be omitted by accident. */\nexport type RouteOptions = RoadRouteOptions | TransitRouteOptions;\n\n/** `TruckProfile` field → the gateway's snake_case query parameter. */\nconst TRUCK_PARAMS: Record<keyof TruckProfile, string> = {\n height: \"height\",\n width: \"width\",\n length: \"length\",\n weight: \"weight\",\n axleLoad: \"axle_load\",\n axleCount: \"axle_count\",\n hazmat: \"hazmat\",\n useTruckRoute: \"use_truck_route\",\n};\n\nfunction truckParams(truck?: TruckProfile): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = String(v);\n }\n return out;\n}\n\n/** `TruckProfile` field to the gateway's snake_case body field, values left as numbers. */\nfunction truckBody(truck?: TruckProfile): Record<string, number | boolean> {\n const out: Record<string, number | boolean> = {};\n if (!truck) return out;\n for (const [field, param] of Object.entries(TRUCK_PARAMS) as [keyof TruckProfile, string][]) {\n const v = truck[field];\n if (v != null) out[param] = v;\n }\n return out;\n}\n\n/**\n * The option fields both POST endpoints accept, shaped for a JSON body.\n *\n * Every option `RouteOptions` declares has to appear here. One left out still typechecks at the\n * call site and is then dropped on the floor, so the caller asks for something and silently does\n * not get it.\n */\nfunction bodyOptions(opts: RouteOptions): Record<string, unknown> {\n return {\n ...(opts.mode ? { mode: opts.mode } : {}),\n ...(opts.departAt != null ? { depart_at: departureValue(opts.departAt) } : {}),\n ...(opts.avoid?.length ? { avoid: opts.avoid } : {}),\n ...(opts.avoidAreas != null ? { avoid_areas: opts.avoidAreas } : {}),\n ...(opts.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\n}\n\nfunction departureValue(value: Date | string): string {\n return value instanceof Date ? value.toISOString() : value;\n}\n\n/**\n * How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a\n * grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description\n * of most of it. `not_modelled` means the routing engine cannot express the rule at all (a\n * conditional restriction); `not_checked` means nothing looked.\n */\nexport type TruckCoverage =\n | \"authoritative\"\n | \"osm_plus_authoritative\"\n | \"osm_only\"\n | \"not_modelled\"\n | \"not_checked\";\n\n/** How much of a truck route's restriction picture is trustworthy, overall. */\nexport type TruckVerification = \"authoritative\" | \"partial\" | \"osm_only\" | \"unknown\" | \"candidate_permit_route\";\n\nexport interface TruckWarning {\n /** Stable machine-readable code, e.g. `INCOMPLETE_VEHICLE_PROFILE`. Switch on this, not the text. */\n code: string;\n message: string;\n}\n\n/** Outcome of the endpoint check. `clear` means clear against what was checked, nothing more. */\nexport type TruckValidationStatus = \"clear\" | \"conflict\" | \"unknown\";\n\nexport interface TruckValidationFinding {\n /**\n * Where the finding applies. The two endpoints are the segments the routing engine does not\n * evaluate; `route` is anywhere along the path, which is where a time-limited restriction lands.\n */\n where: \"origin\" | \"destination\" | \"route\";\n code: string;\n message: string;\n /** The OSM way the finding concerns, when the engine named one. */\n wayId?: number;\n /** For a time-limited finding: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the upstream authority published them. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * What was independently checked after the route was produced.\n *\n * The routing engine does not apply restrictions to a route's first or last segment, because a\n * vehicle has to be able to leave where it is parked. This is the check that looks at those two\n * segments anyway.\n *\n * `conflicts` carries findings that are decided: a posted limit your declared vehicle exceeds, and\n * a full closure in force -- the road is shut whatever the vehicle. `unknowns` carries what could\n * not be decided, which includes a restriction whose kind cannot be compared against dimensions\n * at all (a time window, destination-only access), one whose parameter you did not declare, and\n * an endpoint the engine moved or one that sits on a road closed to trucks.\n *\n * An earlier release of this type said a dimension restriction could only ever be an unknown\n * because the engine did not expose the limit. That was wrong, and comparison against your\n * declared vehicle now happens; send a complete profile and you get real answers rather than\n * \"something is posted here\".\n *\n * `clear` means clear against `checkedRestrictionClasses`; it never means the route is legally\n * compliant.\n *\n * Codes worth branching on, beyond the endpoint ones:\n *\n * - `TEMPORARY_LIMIT_EXCEEDED` (conflict) a time-limited posted limit, in force now, that your\n * declared vehicle exceeds.\n * - `TEMPORARY_LIMIT_SCHEDULED` (unknown) the same for a window that has not started.\n * - `TEMPORARY_LIMIT_DEPENDS_ON_CONFIGURATION` (unknown) a structure posting different weights by\n * vehicle configuration, where your declared mass is over the strictest and within the most\n * permissive. Alberta posts 25 t for a single truck and 63.5 t for a B-train at one bridge, so a\n * declared 40 t is legal as one and illegal as the other. This service does not collect your\n * configuration, so it reports the ambiguity rather than picking one.\n * - `TEMPORARY_LIMIT_UNDECLARED` (unknown) a limit is posted and the parameter it constrains was\n * not declared.\n * - `ENDPOINT_ON_TRUCK_PROHIBITED_ROAD` (unknown) the road at an endpoint is closed to trucks, so\n * the route begins or ends on a different one.\n *\n * Read the message as well as the code on a temporary limit. Some are published by the authority\n * in a structured field and some are read out of its own written description; the message says\n * which, because they do not deserve equal confidence.\n *\n * Check `checkedRestrictionClasses` before trusting a `clear`. When the time-limited layer is\n * unavailable you get `TEMPORAL_RESTRICTIONS_NOT_CHECKED` in `unknowns`, which is a different\n * answer from \"no closures found\".\n */\nexport interface TruckValidation {\n status: TruckValidationStatus;\n /** Exactly what was examined, so you can tell what \"clear\" covered. */\n checkedRestrictionClasses: string[];\n conflicts: TruckValidationFinding[];\n unknowns: TruckValidationFinding[];\n}\n\n/**\n * What a truck route considered, and what it did not. Present on `mode: \"truck\"` results only.\n *\n * Read `warnings` before relying on a route. In particular `ENDPOINT_RESTRICTIONS_UNCHECKED`\n * appears whenever the endpoint check did not clear the route's first and last road segment,\n * which includes every route where the check could not run: restrictions are not applied to\n * those two segments, so clearance and load at the origin and destination must be verified\n * separately. Nothing here ever reports a road as unrestricted; the absence of a restriction\n * means none is known.\n */\nexport interface TruckMeta {\n /** The profile as sent, in the units sent. Never back-filled with the engine's defaults. */\n profile: TruckProfile;\n /** True only when height, width, length, weight and axle load were all supplied. */\n profileComplete: boolean;\n /**\n * Every comparable restriction posted on a road this route actually traversed, whether or not\n * your declared vehicle exceeds it.\n *\n * A statement about the route rather than a list of problems: a 20 t bridge on the path is\n * listed even for a 10 t vehicle, because knowing the route crosses it is worth having. The\n * ones your vehicle does exceed also appear in `validation.conflicts`.\n *\n * `value` is in the engine's unit for that type: metres for `max_height`, `max_width` and\n * `max_length`, tonnes for `max_weight` and `max_axle_load`.\n *\n * Absent rather than `[]` whenever the along-route check did not complete — a route with more\n * distinct roads than one pass can sample, a multi-leg trip, or an engine call that failed.\n * An empty array would say \"nothing is posted on this route\", which is a claim; absent says\n * nothing was established. Check `validation.checkedRestrictionClasses` for\n * `route_static_restrictions` to tell the two apart.\n */\n restrictions_applied?: { type: string; value: number; wayId: number }[];\n coverage: Record<string, TruckCoverage>;\n verification: TruckVerification;\n warnings: TruckWarning[];\n /** The endpoint check. Absent when it did not run. */\n validation?: TruckValidation;\n /**\n * Subdivision codes whose authoritative restrictions are in the deployed graph. A statement\n * about the graph, not about this route: no jurisdiction lookup is performed.\n */\n enrichedJurisdictions?: string[];\n}\n\n/**\n * Something about the route itself that you should know before using it. Every mode.\n *\n * Two things today, both worth reading.\n *\n * The engine moves a requested point to the nearest road it can use, and if there is no road near\n * your point it moves it a long way and still reports success. Asked for a route between two\n * places with no road between them, it answered `Ok` with a confident 10.1 km route having\n * relocated the destination 292.3 km. A route is still returned, because it is usually a good\n * route that merely ends short, but `ENDPOINT_RELOCATED` tells you it does not reach the place you\n * named. This matters most where it is least visible: a well pad, a remote lease, a mine, a new\n * subdivision.\n *\n * And a provincial authority may have a road on your path shut. The graph is rebuilt weekly and\n * cannot carry a closure, so `CLOSURE_IN_FORCE_NEAR_ROUTE` is how you learn that the route may go\n * through one; `CLOSURE_SCHEDULED_NEAR_ROUTE` is one that starts later, with `effectiveFrom`\n * saying when. Reported on every mode, because a shut road is shut to a car too.\n *\n * Branch on `code`, which is stable; the text may be reworded.\n */\nexport interface RouteWarning {\n /** Stable machine-readable code, e.g. `ENDPOINT_RELOCATED`. Switch on this, not the text. */\n code: string;\n message: string;\n /**\n * What it concerns: one of the two endpoints, or `\"route\"` for anything along the path, which\n * is where a closure lands.\n */\n where?: \"origin\" | \"destination\" | \"route\";\n /** For a time-limited warning: in force now, later, or unreadable from the published schedule. */\n when?: \"now\" | \"later\" | \"unknown\";\n /** Epoch seconds, as the authority published them. Either end may be absent. */\n effectiveFrom?: number;\n effectiveTo?: number;\n}\n\n/**\n * The state of the routing graph behind every other call here.\n *\n * `graph` is absent when the engine did not report a build time. That is a real state, not an\n * error: the deployed Canada graph was built before the service was configured to report one.\n * Treat its absence as \"unknown\", never as \"fresh\". `graph.osmTimestamp` is absent until a\n * graph built by the current pipeline is deployed; treat that the same way.\n */\nexport interface RoutingStatus {\n /** The only graph there is. Canada-only is an architectural constraint, not a rollout stage. */\n region: \"canada\";\n /** `unavailable` means the routing container did not answer, not that this call failed. */\n status: \"ready\" | \"unavailable\";\n engine: { name: \"valhalla\"; version?: string };\n graph?: {\n /** When the tileset was last written, ISO 8601. */\n builtAt: string;\n /** Whole hours since `builtAt`. */\n ageHours: number;\n /**\n * When the source OSM extract was current, ISO 8601. Taken from the PBF header\n * (`osmosis_replication_timestamp`) at graph build time. Absent means unknown.\n */\n osmTimestamp?: string;\n };\n /**\n * How fresh each live restriction feed is. The other half of \"is routing current\".\n *\n * The graph is rebuilt weekly and cannot carry a closure, so closures and temporary limits come\n * from feeds refreshed every few minutes instead. A stale feed is why a truck route reports the\n * temporal layer unchecked, and this is where you can see it without inspecting a route.\n *\n * `fresh` means a route would trust it right now. `stale` means it exists but is too old to be\n * trusted, so routes report the layer unchecked. `absent` means no snapshot has ever been\n * written — for a keyed feed that usually means the key is not configured, which is a different\n * thing from decay. `ageSeconds` is omitted for `absent`, because there is nothing to age.\n *\n * Absent from the response entirely when the deployment has no feed store bound.\n */\n restrictionFeeds?: {\n /** Subdivision code, e.g. `CA-ON`. */\n jurisdiction: string;\n state: \"fresh\" | \"stale\" | \"absent\";\n ageSeconds?: number;\n }[];\n}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n /**\n * Route-level warnings, on every mode. Absent when there are none, so a plain route body is\n * unchanged. See `RouteWarning`. Two kinds exist today:\n *\n * - the route does not actually begin or end where you asked (`ENDPOINT_RELOCATED`);\n * - a road closure published by a provincial authority lies within 250 m of the path\n * (`CLOSURE_IN_FORCE_NEAR_ROUTE`, `CLOSURE_SCHEDULED_NEAR_ROUTE`).\n *\n * Closures are reported on every mode, not only on trucks: a washed-out highway is shut to a\n * car as well, and the graph is rebuilt weekly so it cannot carry one. Coverage is Ontario,\n * Alberta and British Columbia, which is where authoritative feeds exist; elsewhere no closure\n * warning means nothing was checked rather than nothing is closed.\n *\n * On `mode: \"truck\"` the same findings also appear inside `truck.validation`, which is a\n * self-contained compliance report and is meant to be readable on its own. The duplication is\n * deliberate: `warnings` is the answer to \"is anything wrong with this route\" for every caller\n * regardless of mode, and putting it somewhere different for trucks would be worse.\n */\n warnings?: RouteWarning[];\n /**\n * Truck restriction coverage and warnings. Present on `mode: \"truck\"` only, and a sibling of\n * the fields above rather than a wrapper around them, so nothing that reads a route today\n * changes. The gateway sends it already; this type ships with the next release.\n *\n * `mode: \"truck\"` itself is included with normal routing on every plan, and applies Valhalla\n * truck costing, the truck restrictions carried in the road network, and the vehicle profile\n * you send. The **Truck Intelligence** add-on gates the layer on top: authoritative Canadian\n * government restriction data, validation against your declared vehicle, and the coverage and\n * provenance reporting.\n *\n * Without the add-on this object is still here and says so. Every `coverage` class reads\n * `not_checked`, `verification` is `unknown`, `validation` is absent rather than an empty\n * report, and `warnings` carries `TRUCK_INTELLIGENCE_NOT_ENABLED`. `not_checked` rather than\n * `osm_only` is deliberate: `osm_only` is a claim about provenance, and without the add-on\n * nothing looked at all.\n *\n * `/route` never refuses over this. Closure warnings in `warnings` above are not gated either,\n * on any mode.\n */\n truck?: TruckMeta;\n /** Present on a multimodal result. Static GTFS is `scheduled`; realtime is a later phase. */\n scheduleType?: \"scheduled\" | \"realtime\";\n /** Walking access/egress and transit rides, in travel order. Present only for transit routes. */\n legs?: RouteLeg[];\n}\n\nexport interface TransitAgencySummary {\n id: string;\n name: string;\n url?: string;\n}\n\nexport interface TransitRouteSummary {\n id: string;\n shortName?: string;\n longName?: string;\n /** CSS hex colour, including `#`. */\n color?: string;\n /** CSS hex text colour, including `#`. */\n textColor?: string;\n}\n\nexport interface TransitStopTime {\n id: string;\n name: string;\n coordinates?: LngLat;\n arrival?: string;\n departure?: string;\n}\n\nexport interface PedestrianRouteLeg {\n mode: \"pedestrian\";\n durationSeconds: number;\n distanceMeters: number;\n}\n\nexport interface TransitRouteLeg {\n mode: \"transit\";\n durationSeconds: number;\n distanceMeters: number;\n agency?: TransitAgencySummary;\n route?: TransitRouteSummary;\n headsign?: string;\n departure?: string;\n arrival?: string;\n stops: TransitStopTime[];\n}\n\nexport type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * **`null` is a real value here, so handle it on every cell.** A pair the engine could not\n * connect is nulled in place and the rest of the grid still answers; the null is never dropped\n * and never shortens a row, so `durations[i][j]` is always `origins[i]` to `destinations[j]`.\n * A `0` is a real answer, which is what the diagonal returns.\n *\n * Treat a `null` as \"no answer for this pair\", never as proof that no road connects the two\n * points. Across 40 pairs probed on the deployed graph, every null was a pair `/route` answers.\n * An earlier version of this comment claimed `null` never appears; that was retracted\n * on 2026-09-20.\n *\n * `matrix()` rejects with a `RouterError` (status 400, code `invalid_location`) when a coordinate\n * is outside WGS84, and with status 422, code `outside_coverage`, when a location is outside\n * Canada, before the engine runs. It rejects with code `no_route` only for failures the\n * engine raises before computing anything: a location with no road near it, a set of locations\n * the road network does not connect (a property of the set, so no one coordinate is at fault),\n * or two locations further apart in a straight line than its matrix limit.\n */\nexport interface MatrixResult {\n /** Seconds. Rows are origins, columns are destinations. */\n durations: (number | null)[][];\n /** Metres, same indexing. */\n distances: (number | null)[][];\n}\n\n/**\n * A route through a set of stops, plus the order to visit them in.\n *\n * A route with one extra field rather than a shape of its own, the same reasoning that makes\n * `match()` return `RouteResult` outright: an optimized route is a route.\n */\nexport interface OptimizedRouteResult extends RouteResult {\n /** Indices into the request's `stops`, in visiting order. Always a permutation of them. */\n order: number[];\n}\n\n/** Minimal GeoJSON FeatureCollection (isochrone bands). */\nexport interface FeatureCollection {\n type: \"FeatureCollection\";\n features: unknown[];\n}\n\n/**\n * Travel-time bands around a point. Transit bands ride scheduled transit from `departAt`, and a\n * band can be a `MultiPolygon`: the area around the stop a ride drops you at is often its own\n * island. A centre with no scheduled transit nearby is a `RouterError` with code\n * `outside_coverage`, not a walking-only polygon.\n */\nexport type IsochroneOptions = (RoadRouteOptions | TransitRouteOptions) & {\n /** Travel-time bands in minutes. At most four, each 120 or less. */\n minutes: number[];\n};\n\n/**\n * A routing failure.\n *\n * Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside\n * every other unmap client's errors. `status` and `code` carry the HTTP status and the\n * gateway's error code, and `retryAfterSeconds` comes from the `Retry-After` header.\n */\nexport class RouterError extends UnmapError {\n constructor(status: number, message: string, code?: string, retryAfterSeconds?: number) {\n super(status, message, code, retryAfterSeconds);\n this.name = \"RouterError\";\n }\n}\n\n/**\n * The gateway answers every error with a JSON body whose `error` says what went wrong\n * (\"mode must be one of auto, car, bicycle, pedestrian, truck, transit\", \"q required\"). Put that in the\n * message so a caller who logs the thrown error reads the reason, not just the status.\n */\nasync function errorFrom(res: { status: number; headers?: Headers; json(): Promise<unknown> }, path: string): Promise<RouterError> {\n const fallback = `route ${res.status} on ${path}`;\n let body: { error?: unknown; code?: unknown } | undefined;\n try {\n body = (await res.json()) as { error?: unknown; code?: unknown };\n } catch {\n body = undefined;\n }\n const reason = typeof body?.error === \"string\" ? body.error : undefined;\n const code = typeof body?.code === \"string\" ? body.code : undefined;\n const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);\n}\n\n/** Typed client for the unmap routing API (gateway `/route`, `/isochrone`, `/matrix`, `/match`, `/optimized-route`). */\nexport class Router {\n private readonly key: string;\n private readonly base: string;\n private readonly fetchImpl: typeof fetch;\n private readonly retry: RetryPolicy | false | undefined;\n\n constructor(opts: RouterOptions) {\n this.retry = opts.retry;\n this.key = opts.key;\n this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n // Bound to globalThis: stored on an instance and called as `this.fetchImpl(...)`, a bare\n // `fetch` reference is invoked with the Router as its receiver, and browsers reject that\n // with \"Illegal invocation\". Node is lenient, so this only ever surfaces in the browser.\n this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n route(from: LngLat, to: LngLat, opts: RouteOptions = {}): Promise<RouteResult> {\n // A polygon cannot ride a query string, so an `avoidAreas` call switches verb. Not a\n // behaviour change the caller has to think about: same parameters, same response, and the\n // GET stays the default so a plain route keeps its cacheable URL.\n if (opts.avoidAreas != null) {\n return this.post<RouteResult>(\"/route\", { from, to, ...bodyOptions(opts) }, opts.signal);\n }\n return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n isochrone(near: LngLat, opts: IsochroneOptions): Promise<FeatureCollection> {\n if (opts.avoidAreas != null) {\n return this.post<FeatureCollection>(\n \"/isochrone\",\n { near, minutes: opts.minutes, ...bodyOptions(opts) },\n opts.signal,\n );\n }\n return this.get<FeatureCollection>(\n \"/isochrone\",\n {\n near: near.join(\",\"),\n minutes: opts.minutes.join(\",\"),\n mode: opts.mode,\n depart_at: opts.departAt != null ? departureValue(opts.departAt) : undefined,\n avoid: opts.avoid?.length ? opts.avoid.join(\",\") : undefined,\n use_highways: opts.useHighways != null ? String(opts.useHighways) : undefined,\n use_hills: opts.useHills != null ? String(opts.useHills) : undefined,\n ...truckParams(opts.truck),\n },\n opts.signal,\n );\n }\n\n /**\n * The routing graph's state: whether the engine is answering, which version it is, and how\n * old the Canada graph is.\n *\n * Resolves rather than rejects when routing is down: it answers `status: \"unavailable\"`,\n * because a status call that throws when the subject is unhealthy tells you nothing a failed\n * `route()` had not already told you.\n */\n status(opts: { signal?: AbortSignal } = {}): Promise<RoutingStatus> {\n return this.get<RoutingStatus>(\"/routing/status\", {}, opts.signal);\n }\n\n /**\n * Travel times and distances between every origin and every destination.\n *\n * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get\n * where an origin is also a destination, and a `null` is a real answer too: a pair the engine\n * could not connect is nulled in place while the rest of the grid answers. A coordinate outside\n * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside\n * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise\n * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a\n * set of locations the road network does not connect, or two locations past its straight-line\n * matrix limit.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RoadRouteOptions = {}): Promise<MatrixResult> {\n return this.post<MatrixResult>(\"/matrix\", { origins, destinations, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.\n *\n * Returns the same `RouteResult` `route()` returns: a matched trace is a route. A coordinate\n * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada\n * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.\n */\n match(coordinates: LngLat[], opts: RoadRouteOptions = {}): Promise<RouteResult> {\n return this.post<RouteResult>(\"/match\", { coordinates, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Order a set of stops and return the route through them. At least 3 and at most 10 stops.\n *\n * The first stop is the fixed start and the last is the fixed end; only the stops between\n * them are reordered. There is no round-trip or open-route toggle: that is a real constraint\n * of the underlying engine, not an omission here.\n *\n * Optimizing solves a cost matrix over every stop, but it does not inherit `matrix()`'s per-cell\n * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84\n * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects\n * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with\n * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network\n * connects, or two are further apart than the engine's straight-line matrix limit. There is no\n * partial tour either way, because a tour needs every leg.\n */\n optimize(stops: LngLat[], opts: RoadRouteOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n private async post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n const res = await fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n\n private async get<T>(\n path: string,\n params: Record<string, string | undefined>,\n signal?: AbortSignal,\n ): Promise<T> {\n const q = new URLSearchParams({ key: this.key });\n for (const [k, v] of Object.entries(params)) if (v != null && v !== \"\") q.set(k, v);\n const res = await fetchWithRetry(\n this.fetchImpl,\n `${this.base}${path}?${q}`,\n { signal, headers: { accept: \"application/json\" } },\n this.retry,\n );\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n\n// The shared error type and retry policy, re-exported so a caller who installed only this\n// package can `catch (e) { if (e instanceof UnmapError) }` without adding @unmap/core.\nexport { UnmapError, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\n"],"mappings":"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","names":["UnmapError","parseRetryAfter"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unmap/routing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Typed client for the unmap routing API (route + isochrone)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@unmap/core": "0.
|
|
41
|
+
"@unmap/core": "0.18.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"tsup": "^8.3.0",
|