@unmap/routing 0.12.0 → 0.14.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @unmap/routing
2
2
 
3
3
  Typed client for the [unmap](https://unmap.dev) routing API: point-to-point routes and
4
- travel-time isochrones over the Canadian road graph, for driving, trucking, cycling, and walking. No map
5
- dependency; works in Node and the browser.
4
+ travel-time isochrones over the Canadian road graph, for driving, trucking, cycling, and walking,
5
+ plus the departure-time transit route contract. No map dependency; works in Node and the browser.
6
6
 
7
7
  ```bash
8
8
  npm i @unmap/routing
@@ -22,13 +22,19 @@ const bands = await router.isochrone([-114.07, 51.05], { minutes: [10, 20], mode
22
22
  bands.features // one GeoJSON polygon per minute value
23
23
  ```
24
24
 
25
- Coordinates are always `[lng, lat]`. `mode` is `'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck'` and defaults
25
+ Coordinates are always `[lng, lat]`. `mode` is `'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck' | 'transit'` and defaults
26
26
  to `'auto'`; `minutes` is required and takes up to four values of at most 120 minutes. Driving modes accept
27
27
  `useHighways` (`0` avoid motorways and trunks, `1` prefer); cycling and walking accept `useHills`
28
28
  (`0` avoid climbs even for a longer route, `1` indifferent) and already account for terrain
29
29
  without it. Driving times do not vary with grade at all, which is Valhalla's model, so
30
30
  `useHills` on a driving mode is a `400` rather than a no-op. Both methods accept an
31
- `AbortSignal` as `signal`. A non-2xx response throws a `RouterError` carrying the HTTP `status`.
31
+ `AbortSignal` as `signal`. A non-2xx response throws a `RouterError` carrying the HTTP `status`
32
+ and, when the body has one, `code`. An endpoint outside Canada is `422` `outside_coverage`.
33
+ A longitude or latitude outside WGS84 is `400` `invalid_location`, not that code.
34
+ A Canadian trip the graph cannot connect, including Iqaluit to the mainland, is `422` `no_route`.
35
+
36
+ Transit requires `departAt` and is route-only. Production answers TTC trips. There is no
37
+ `arriveBy`, transit matrix, or realtime claim. Other static feeds stay schedule-only.
32
38
 
33
39
  Parameters, response shapes, and error bodies are documented at
34
- [unmap.dev/docs/routing-api](https://unmap.dev/docs/routing-api).
40
+ [unmap.dev/docs/apis/routing](https://unmap.dev/docs/apis/routing).
package/dist/index.d.ts CHANGED
@@ -1,10 +1,24 @@
1
+ import { RetryPolicy, UnmapError } from '@unmap/core';
2
+ export { RetryPolicy, UnmapError, parseRetryAfter } from '@unmap/core';
3
+
1
4
  interface RouterOptions {
2
5
  key: string;
3
6
  gateway?: string;
4
7
  fetch?: typeof fetch;
8
+ /**
9
+ * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it.
10
+ *
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 uses `routing_failed`, and `/elevation` uses
13
+ * `upstream_error`. With no header the client backs off itself. A wait longer than
14
+ * `maxDelayMs` (10 s) is not slept through; the error carries `retryAfterSeconds` instead.
15
+ * See `RetryPolicy` in `@unmap/core`.
16
+ */
17
+ retry?: RetryPolicy | false;
5
18
  }
6
19
  /** `car` is the same costing as `auto`, spelled for people who do not think in Valhalla terms. */
7
- type TravelMode = "auto" | "car" | "bicycle" | "pedestrian" | "truck";
20
+ type TravelMode = "auto" | "car" | "bicycle" | "pedestrian" | "truck" | "transit";
21
+ type RoadTravelMode = Exclude<TravelMode, "transit">;
8
22
  /** [longitude, latitude]. */
9
23
  type LngLat = [number, number];
10
24
  /**
@@ -30,8 +44,86 @@ interface TruckProfile {
30
44
  /** Preference for designated truck routes, 0 (none) to 1 (strong). */
31
45
  useTruckRoute?: number;
32
46
  }
33
- interface RouteOptions {
34
- mode?: TravelMode;
47
+ /**
48
+ * A road feature to weight against. `tolls` and `highways` are driving modes only; `ferries`
49
+ * applies to every road mode. `unpaved` is deliberately absent: Valhalla has no surface term in its
50
+ * driving costing, so it could only ever have meant two different things by mode.
51
+ */
52
+ type AvoidToken = "tolls" | "highways" | "ferries";
53
+ /** A GeoJSON position. The gateway requires exactly `[longitude, latitude]` at runtime. */
54
+ type AvoidAreaPosition = number[];
55
+ /** A closed GeoJSON linear ring. */
56
+ type AvoidAreaRing = AvoidAreaPosition[];
57
+ /** Polygon with one exterior ring. Interior rings are rejected by the gateway. */
58
+ interface AvoidAreaPolygon {
59
+ type: "Polygon";
60
+ coordinates: AvoidAreaRing[];
61
+ }
62
+ /** Multiple polygons, each with one exterior ring. */
63
+ interface AvoidAreaMultiPolygon {
64
+ type: "MultiPolygon";
65
+ coordinates: AvoidAreaRing[][];
66
+ }
67
+ interface AvoidAreaFeature {
68
+ type: "Feature";
69
+ geometry: AvoidAreaPolygon | AvoidAreaMultiPolygon;
70
+ properties?: Record<string, unknown> | null;
71
+ }
72
+ interface AvoidAreaFeatureCollection {
73
+ type: "FeatureCollection";
74
+ features: AvoidAreaFeature[];
75
+ }
76
+ type AvoidAreas = AvoidAreaPolygon | AvoidAreaMultiPolygon | AvoidAreaFeature | AvoidAreaFeatureCollection;
77
+ interface SharedRouteOptions {
78
+ signal?: AbortSignal;
79
+ }
80
+ interface TransitRouteOptions extends SharedRouteOptions {
81
+ mode: "transit";
82
+ /**
83
+ * Scheduled departure. A Date is serialized as an ISO instant; a
84
+ * string is sent unchanged so callers can provide an ISO value with an explicit local offset.
85
+ * Transit supports departure time only. There is no `arriveBy` option.
86
+ */
87
+ departAt: Date | string;
88
+ avoid?: never;
89
+ avoidAreas?: never;
90
+ truck?: never;
91
+ useHighways?: never;
92
+ useHills?: never;
93
+ }
94
+ interface RoadRouteOptions extends SharedRouteOptions {
95
+ mode?: RoadTravelMode;
96
+ /** A scheduled departure applies only to `mode: "transit"`. */
97
+ departAt?: never;
98
+ /**
99
+ * Road features to weight against.
100
+ *
101
+ * **A preference, not an exclusion.** These set Valhalla costing weights to 0, which
102
+ * penalises the feature heavily but does not forbid it: a route whose only path crosses a
103
+ * toll bridge still crosses it, and nothing in the result says that it did. If the
104
+ * distinction matters to you, check the returned geometry. Avoid areas have a separate,
105
+ * feature-gated API because they use Valhalla's exclusion mechanism rather than a weight.
106
+ *
107
+ * `tolls` and `highways` are driving modes only (`auto`, `car`, `truck`); `ferries` applies
108
+ * to every road mode. `avoid: ["highways"]` and `useHighways` are the same knob at different
109
+ * resolutions and the gateway rejects both together.
110
+ */
111
+ avoid?: AvoidToken[];
112
+ /**
113
+ * Areas to keep the route out of, as a GeoJSON `Polygon`, `MultiPolygon`, or
114
+ * `FeatureCollection` of either.
115
+ *
116
+ * Valhalla defines these as exclusions rather than preferences. Server support remains behind
117
+ * a feature flag while a defect in the pinned engine can snap a location across the polygon;
118
+ * do not rely on this option until the service documentation says the flag is enabled.
119
+ * Interior rings are rejected because the engine cannot preserve holes.
120
+ *
121
+ * At most 8 areas, 100 vertices and 10 km of perimeter in total.
122
+ *
123
+ * Sending this makes `route()` and `isochrone()` use POST instead of GET, because a polygon
124
+ * cannot ride a query string. The response is identical either way.
125
+ */
126
+ avoidAreas?: AvoidAreas;
35
127
  /** Only meaningful with `mode: "truck"`; the gateway rejects it on any other mode. */
36
128
  truck?: TruckProfile;
37
129
  /**
@@ -47,14 +139,182 @@ interface RouteOptions {
47
139
  * baked into the Canada graph at build time.
48
140
  */
49
141
  useHills?: number;
50
- signal?: AbortSignal;
142
+ }
143
+ /** Route-only transit options are a separate branch so `departAt` cannot be omitted by accident. */
144
+ type RouteOptions = RoadRouteOptions | TransitRouteOptions;
145
+ /**
146
+ * How well a class of truck restriction is covered for a route. `osm_only` is provenance, not a
147
+ * grade: Canada publishes no national bridge inventory, so OSM-derived is the honest description
148
+ * of most of it. `not_modelled` means the routing engine cannot express the rule at all (a
149
+ * conditional restriction); `not_checked` means nothing looked.
150
+ */
151
+ type TruckCoverage = "authoritative" | "osm_plus_authoritative" | "osm_only" | "not_modelled" | "not_checked";
152
+ /** How much of a truck route's restriction picture is trustworthy, overall. */
153
+ type TruckVerification = "authoritative" | "partial" | "osm_only" | "unknown" | "candidate_permit_route";
154
+ interface TruckWarning {
155
+ /** Stable machine-readable code, e.g. `INCOMPLETE_VEHICLE_PROFILE`. Switch on this, not the text. */
156
+ code: string;
157
+ message: string;
158
+ }
159
+ /** Outcome of the endpoint check. `clear` means clear against what was checked, nothing more. */
160
+ type TruckValidationStatus = "clear" | "conflict" | "unknown";
161
+ interface TruckValidationFinding {
162
+ /**
163
+ * Where the finding applies. The two endpoints are the segments the routing engine does not
164
+ * evaluate; `route` is anywhere along the path, which is where a time-limited restriction lands.
165
+ */
166
+ where: "origin" | "destination" | "route";
167
+ code: string;
168
+ message: string;
169
+ /** The OSM way the finding concerns, when the engine named one. */
170
+ wayId?: number;
171
+ /** For a time-limited finding: in force now, later, or unreadable from the published schedule. */
172
+ when?: "now" | "later" | "unknown";
173
+ /** Epoch seconds, as the upstream authority published them. */
174
+ effectiveFrom?: number;
175
+ effectiveTo?: number;
176
+ }
177
+ /**
178
+ * What was independently checked after the route was produced.
179
+ *
180
+ * The routing engine does not apply restrictions to a route's first or last segment, because a
181
+ * vehicle has to be able to leave where it is parked. This is the check that looks at those two
182
+ * segments anyway.
183
+ *
184
+ * `conflicts` carries findings that are decided: a posted limit your declared vehicle exceeds, and
185
+ * a full closure in force -- the road is shut whatever the vehicle. `unknowns` carries what could
186
+ * not be decided, which includes a restriction whose kind cannot be compared against dimensions
187
+ * at all (a time window, destination-only access), one whose parameter you did not declare, and
188
+ * an endpoint the engine moved or one that sits on a road closed to trucks.
189
+ *
190
+ * An earlier release of this type said a dimension restriction could only ever be an unknown
191
+ * because the engine did not expose the limit. That was wrong, and comparison against your
192
+ * declared vehicle now happens; send a complete profile and you get real answers rather than
193
+ * "something is posted here".
194
+ *
195
+ * `clear` means clear against `checkedRestrictionClasses`; it never means the route is legally
196
+ * compliant.
197
+ *
198
+ * Codes worth branching on, beyond the endpoint ones:
199
+ *
200
+ * - `TEMPORARY_LIMIT_EXCEEDED` (conflict) a time-limited posted limit, in force now, that your
201
+ * declared vehicle exceeds.
202
+ * - `TEMPORARY_LIMIT_SCHEDULED` (unknown) the same for a window that has not started.
203
+ * - `TEMPORARY_LIMIT_DEPENDS_ON_CONFIGURATION` (unknown) a structure posting different weights by
204
+ * vehicle configuration, where your declared mass is over the strictest and within the most
205
+ * permissive. Alberta posts 25 t for a single truck and 63.5 t for a B-train at one bridge, so a
206
+ * declared 40 t is legal as one and illegal as the other. This service does not collect your
207
+ * configuration, so it reports the ambiguity rather than picking one.
208
+ * - `TEMPORARY_LIMIT_UNDECLARED` (unknown) a limit is posted and the parameter it constrains was
209
+ * not declared.
210
+ * - `ENDPOINT_ON_TRUCK_PROHIBITED_ROAD` (unknown) the road at an endpoint is closed to trucks, so
211
+ * the route begins or ends on a different one.
212
+ *
213
+ * Read the message as well as the code on a temporary limit. Some are published by the authority
214
+ * in a structured field and some are read out of its own written description; the message says
215
+ * which, because they do not deserve equal confidence.
216
+ *
217
+ * Check `checkedRestrictionClasses` before trusting a `clear`. When the time-limited layer is
218
+ * unavailable you get `TEMPORAL_RESTRICTIONS_NOT_CHECKED` in `unknowns`, which is a different
219
+ * answer from "no closures found".
220
+ */
221
+ interface TruckValidation {
222
+ status: TruckValidationStatus;
223
+ /** Exactly what was examined, so you can tell what "clear" covered. */
224
+ checkedRestrictionClasses: string[];
225
+ conflicts: TruckValidationFinding[];
226
+ unknowns: TruckValidationFinding[];
227
+ }
228
+ /**
229
+ * What a truck route considered, and what it did not. Present on `mode: "truck"` results only.
230
+ *
231
+ * Read `warnings` before relying on a route. In particular `ENDPOINT_RESTRICTIONS_UNCHECKED`
232
+ * appears whenever the endpoint check did not clear the route's first and last road segment,
233
+ * which includes every route where the check could not run: restrictions are not applied to
234
+ * those two segments, so clearance and load at the origin and destination must be verified
235
+ * separately. Nothing here ever reports a road as unrestricted; the absence of a restriction
236
+ * means none is known.
237
+ */
238
+ interface TruckMeta {
239
+ /** The profile as sent, in the units sent. Never back-filled with the engine's defaults. */
240
+ profile: TruckProfile;
241
+ /** True only when height, width, length, weight and axle load were all supplied. */
242
+ profileComplete: boolean;
243
+ /**
244
+ * Every comparable restriction posted on a road this route actually traversed, whether or not
245
+ * your declared vehicle exceeds it.
246
+ *
247
+ * A statement about the route rather than a list of problems: a 20 t bridge on the path is
248
+ * listed even for a 10 t vehicle, because knowing the route crosses it is worth having. The
249
+ * ones your vehicle does exceed also appear in `validation.conflicts`.
250
+ *
251
+ * `value` is in the engine's unit for that type: metres for `max_height`, `max_width` and
252
+ * `max_length`, tonnes for `max_weight` and `max_axle_load`.
253
+ *
254
+ * Absent rather than `[]` whenever the along-route check did not complete — a route with more
255
+ * distinct roads than one pass can sample, a multi-leg trip, or an engine call that failed.
256
+ * An empty array would say "nothing is posted on this route", which is a claim; absent says
257
+ * nothing was established. Check `validation.checkedRestrictionClasses` for
258
+ * `route_static_restrictions` to tell the two apart.
259
+ */
260
+ restrictions_applied?: {
261
+ type: string;
262
+ value: number;
263
+ wayId: number;
264
+ }[];
265
+ coverage: Record<string, TruckCoverage>;
266
+ verification: TruckVerification;
267
+ warnings: TruckWarning[];
268
+ /** The endpoint check. Absent when it did not run. */
269
+ validation?: TruckValidation;
270
+ /**
271
+ * Subdivision codes whose authoritative restrictions are in the deployed graph. A statement
272
+ * about the graph, not about this route: no jurisdiction lookup is performed.
273
+ */
274
+ enrichedJurisdictions?: string[];
275
+ }
276
+ /**
277
+ * Something about the route itself that you should know before using it. Every mode.
278
+ *
279
+ * Two things today, both worth reading.
280
+ *
281
+ * The engine moves a requested point to the nearest road it can use, and if there is no road near
282
+ * your point it moves it a long way and still reports success. Asked for a route between two
283
+ * places with no road between them, it answered `Ok` with a confident 10.1 km route having
284
+ * relocated the destination 292.3 km. A route is still returned, because it is usually a good
285
+ * route that merely ends short, but `ENDPOINT_RELOCATED` tells you it does not reach the place you
286
+ * named. This matters most where it is least visible: a well pad, a remote lease, a mine, a new
287
+ * subdivision.
288
+ *
289
+ * And a provincial authority may have a road on your path shut. The graph is rebuilt weekly and
290
+ * cannot carry a closure, so `CLOSURE_IN_FORCE_NEAR_ROUTE` is how you learn that the route may go
291
+ * through one; `CLOSURE_SCHEDULED_NEAR_ROUTE` is one that starts later, with `effectiveFrom`
292
+ * saying when. Reported on every mode, because a shut road is shut to a car too.
293
+ *
294
+ * Branch on `code`, which is stable; the text may be reworded.
295
+ */
296
+ interface RouteWarning {
297
+ /** Stable machine-readable code, e.g. `ENDPOINT_RELOCATED`. Switch on this, not the text. */
298
+ code: string;
299
+ message: string;
300
+ /**
301
+ * What it concerns: one of the two endpoints, or `"route"` for anything along the path, which
302
+ * is where a closure lands.
303
+ */
304
+ where?: "origin" | "destination" | "route";
305
+ /** For a time-limited warning: in force now, later, or unreadable from the published schedule. */
306
+ when?: "now" | "later" | "unknown";
307
+ /** Epoch seconds, as the authority published them. Either end may be absent. */
308
+ effectiveFrom?: number;
309
+ effectiveTo?: number;
51
310
  }
52
311
  /**
53
312
  * The state of the routing graph behind every other call here.
54
313
  *
55
314
  * `graph` is absent when the engine did not report a build time. That is a real state, not an
56
315
  * error: the deployed Canada graph was built before the service was configured to report one.
57
- * Treat its absence as "unknown", never as "fresh".
316
+ * Treat its absence as "unknown", never as "fresh". `graph.osmTimestamp` is absent until a
317
+ * graph built by the current pipeline is deployed; treat that the same way.
58
318
  */
59
319
  interface RoutingStatus {
60
320
  /** The only graph there is. Canada-only is an architectural constraint, not a rollout stage. */
@@ -70,7 +330,32 @@ interface RoutingStatus {
70
330
  builtAt: string;
71
331
  /** Whole hours since `builtAt`. */
72
332
  ageHours: number;
333
+ /**
334
+ * When the source OSM extract was current, ISO 8601. Taken from the PBF header
335
+ * (`osmosis_replication_timestamp`) at graph build time. Absent means unknown.
336
+ */
337
+ osmTimestamp?: string;
73
338
  };
339
+ /**
340
+ * How fresh each live restriction feed is. The other half of "is routing current".
341
+ *
342
+ * The graph is rebuilt weekly and cannot carry a closure, so closures and temporary limits come
343
+ * from feeds refreshed every few minutes instead. A stale feed is why a truck route reports the
344
+ * temporal layer unchecked, and this is where you can see it without inspecting a route.
345
+ *
346
+ * `fresh` means a route would trust it right now. `stale` means it exists but is too old to be
347
+ * trusted, so routes report the layer unchecked. `absent` means no snapshot has ever been
348
+ * written — for a keyed feed that usually means the key is not configured, which is a different
349
+ * thing from decay. `ageSeconds` is omitted for `absent`, because there is nothing to age.
350
+ *
351
+ * Absent from the response entirely when the deployment has no feed store bound.
352
+ */
353
+ restrictionFeeds?: {
354
+ /** Subdivision code, e.g. `CA-ON`. */
355
+ jurisdiction: string;
356
+ state: "fresh" | "stale" | "absent";
357
+ ageSeconds?: number;
358
+ }[];
74
359
  }
75
360
  interface RouteResult {
76
361
  distanceMeters: number;
@@ -79,15 +364,108 @@ interface RouteResult {
79
364
  type: "LineString";
80
365
  coordinates: LngLat[];
81
366
  };
367
+ /**
368
+ * Route-level warnings, on every mode. Absent when there are none, so a plain route body is
369
+ * unchanged. See `RouteWarning`. Two kinds exist today:
370
+ *
371
+ * - the route does not actually begin or end where you asked (`ENDPOINT_RELOCATED`);
372
+ * - a road closure published by a provincial authority lies within 250 m of the path
373
+ * (`CLOSURE_IN_FORCE_NEAR_ROUTE`, `CLOSURE_SCHEDULED_NEAR_ROUTE`).
374
+ *
375
+ * Closures are reported on every mode, not only on trucks: a washed-out highway is shut to a
376
+ * car as well, and the graph is rebuilt weekly so it cannot carry one. Coverage is Ontario,
377
+ * Alberta and British Columbia, which is where authoritative feeds exist; elsewhere no closure
378
+ * warning means nothing was checked rather than nothing is closed.
379
+ *
380
+ * On `mode: "truck"` the same findings also appear inside `truck.validation`, which is a
381
+ * self-contained compliance report and is meant to be readable on its own. The duplication is
382
+ * deliberate: `warnings` is the answer to "is anything wrong with this route" for every caller
383
+ * regardless of mode, and putting it somewhere different for trucks would be worse.
384
+ */
385
+ warnings?: RouteWarning[];
386
+ /**
387
+ * Truck restriction coverage and warnings. Present on `mode: "truck"` only, and a sibling of
388
+ * the fields above rather than a wrapper around them, so nothing that reads a route today
389
+ * changes. The gateway sends it already; this type ships with the next release.
390
+ *
391
+ * `mode: "truck"` itself is included with normal routing on every plan, and applies Valhalla
392
+ * truck costing, the truck restrictions carried in the road network, and the vehicle profile
393
+ * you send. The **Truck Intelligence** add-on gates the layer on top: authoritative Canadian
394
+ * government restriction data, validation against your declared vehicle, and the coverage and
395
+ * provenance reporting.
396
+ *
397
+ * Without the add-on this object is still here and says so. Every `coverage` class reads
398
+ * `not_checked`, `verification` is `unknown`, `validation` is absent rather than an empty
399
+ * report, and `warnings` carries `TRUCK_INTELLIGENCE_NOT_ENABLED`. `not_checked` rather than
400
+ * `osm_only` is deliberate: `osm_only` is a claim about provenance, and without the add-on
401
+ * nothing looked at all.
402
+ *
403
+ * `/route` never refuses over this. Closure warnings in `warnings` above are not gated either,
404
+ * on any mode.
405
+ */
406
+ truck?: TruckMeta;
407
+ /** Present on a multimodal result. Static GTFS is `scheduled`; realtime is a later phase. */
408
+ scheduleType?: "scheduled" | "realtime";
409
+ /** Walking access/egress and transit rides, in travel order. Present only for transit routes. */
410
+ legs?: RouteLeg[];
411
+ }
412
+ interface TransitAgencySummary {
413
+ id: string;
414
+ name: string;
415
+ url?: string;
416
+ }
417
+ interface TransitRouteSummary {
418
+ id: string;
419
+ shortName?: string;
420
+ longName?: string;
421
+ /** CSS hex colour, including `#`. */
422
+ color?: string;
423
+ /** CSS hex text colour, including `#`. */
424
+ textColor?: string;
425
+ }
426
+ interface TransitStopTime {
427
+ id: string;
428
+ name: string;
429
+ coordinates?: LngLat;
430
+ arrival?: string;
431
+ departure?: string;
82
432
  }
433
+ interface PedestrianRouteLeg {
434
+ mode: "pedestrian";
435
+ durationSeconds: number;
436
+ distanceMeters: number;
437
+ }
438
+ interface TransitRouteLeg {
439
+ mode: "transit";
440
+ durationSeconds: number;
441
+ distanceMeters: number;
442
+ agency?: TransitAgencySummary;
443
+ route?: TransitRouteSummary;
444
+ headsign?: string;
445
+ departure?: string;
446
+ arrival?: string;
447
+ stops: TransitStopTime[];
448
+ }
449
+ type RouteLeg = PedestrianRouteLeg | TransitRouteLeg;
83
450
  /**
84
451
  * Travel times and distances between every origin and every destination.
85
452
  *
86
- * If any pair is unreachable, or beyond the engine's own road-distance limit, `matrix()`
87
- * rejects with a `RouterError` (status 422, code `no_route`) instead of resolving with a
88
- * `null` cell for that pair: `null` never appears in a resolved `MatrixResult` in practice. It
89
- * stays in the type as a defensive case, costing nothing and remaining correct if a future
90
- * engine version starts nulling cells instead of failing the request.
453
+ * **`null` is a real value here, so handle it on every cell.** A pair the engine could not
454
+ * connect is nulled in place and the rest of the grid still answers; the null is never dropped
455
+ * and never shortens a row, so `durations[i][j]` is always `origins[i]` to `destinations[j]`.
456
+ * A `0` is a real answer, which is what the diagonal returns.
457
+ *
458
+ * Treat a `null` as "no answer for this pair", never as proof that no road connects the two
459
+ * points. Across 40 pairs probed on the deployed graph, every null was a pair `/route` answers.
460
+ * An earlier version of this comment claimed `null` never appears; that was retracted
461
+ * on 2026-09-20.
462
+ *
463
+ * `matrix()` rejects with a `RouterError` (status 400, code `invalid_location`) when a coordinate
464
+ * is outside WGS84, and with status 422, code `outside_coverage`, when a location is outside
465
+ * Canada, before the engine runs. It rejects with code `no_route` only for failures the
466
+ * engine raises before computing anything: a location with no road near it, a set of locations
467
+ * the road network does not connect (a property of the set, so no one coordinate is at fault),
468
+ * or two locations further apart in a straight line than its matrix limit.
91
469
  */
92
470
  interface MatrixResult {
93
471
  /** Seconds. Rows are origins, columns are destinations. */
@@ -110,22 +488,25 @@ interface FeatureCollection {
110
488
  type: "FeatureCollection";
111
489
  features: unknown[];
112
490
  }
113
- interface IsochroneOptions extends RouteOptions {
491
+ interface IsochroneOptions extends RoadRouteOptions {
114
492
  minutes: number[];
115
493
  }
116
- declare class RouterError extends Error {
117
- readonly status: number;
118
- /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */
119
- readonly code?: string | undefined;
120
- constructor(status: number, message: string,
121
- /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */
122
- code?: string | undefined);
494
+ /**
495
+ * A routing failure.
496
+ *
497
+ * Extends `UnmapError`, so `catch (e) { if (e instanceof UnmapError) }` catches this alongside
498
+ * every other unmap client's errors. `instanceof RouterError` keeps working, and `status` and `code`
499
+ * are still where they were; `retryAfterSeconds` is new and comes from the header.
500
+ */
501
+ declare class RouterError extends UnmapError {
502
+ constructor(status: number, message: string, code?: string, retryAfterSeconds?: number);
123
503
  }
124
504
  /** Typed client for the unmap routing API (gateway `/route`, `/isochrone`, `/matrix`, `/match`, `/optimized-route`). */
125
505
  declare class Router {
126
506
  private readonly key;
127
507
  private readonly base;
128
508
  private readonly fetchImpl;
509
+ private readonly retry;
129
510
  constructor(opts: RouterOptions);
130
511
  route(from: LngLat, to: LngLat, opts?: RouteOptions): Promise<RouteResult>;
131
512
  isochrone(center: LngLat, opts: IsochroneOptions): Promise<FeatureCollection>;
@@ -144,17 +525,23 @@ declare class Router {
144
525
  * Travel times and distances between every origin and every destination.
145
526
  *
146
527
  * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get
147
- * where an origin is also a destination. If any pair is unreachable, or beyond the engine's
148
- * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)
149
- * rather than resolving with a `null` cell for that pair.
528
+ * where an origin is also a destination, and a `null` is a real answer too: a pair the engine
529
+ * could not connect is nulled in place while the rest of the grid answers. A coordinate outside
530
+ * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside
531
+ * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise
532
+ * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a
533
+ * set of locations the road network does not connect, or two locations past its straight-line
534
+ * matrix limit.
150
535
  */
151
- matrix(origins: LngLat[], destinations: LngLat[], opts?: RouteOptions): Promise<MatrixResult>;
536
+ matrix(origins: LngLat[], destinations: LngLat[], opts?: RoadRouteOptions): Promise<MatrixResult>;
152
537
  /**
153
538
  * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.
154
539
  *
155
- * Returns the same `RouteResult` `route()` returns: a matched trace is a route.
540
+ * Returns the same `RouteResult` `route()` returns: a matched trace is a route. A coordinate
541
+ * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada
542
+ * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.
156
543
  */
157
- match(coordinates: LngLat[], opts?: RouteOptions): Promise<RouteResult>;
544
+ match(coordinates: LngLat[], opts?: RoadRouteOptions): Promise<RouteResult>;
158
545
  /**
159
546
  * Order a set of stops and return the route through them. At least 3 and at most 10 stops.
160
547
  *
@@ -162,14 +549,17 @@ declare class Router {
162
549
  * them are reordered. There is no round-trip or open-route toggle: that is a real constraint
163
550
  * of the underlying engine, not an omission here.
164
551
  *
165
- * Optimizing solves a cost matrix over every stop, so it is expected to fail the same way `matrix()` does:
166
- * if any stop is unreachable, or the set is spread further apart than the engine's own
167
- * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)
168
- * rather than resolving with a partial tour.
552
+ * Optimizing solves a cost matrix over every stop, but it does not inherit `matrix()`'s per-cell
553
+ * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84
554
+ * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects
555
+ * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with
556
+ * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network
557
+ * connects, or two are further apart than the engine's straight-line matrix limit. There is no
558
+ * partial tour either way, because a tour needs every leg.
169
559
  */
170
- optimize(stops: LngLat[], opts?: RouteOptions): Promise<OptimizedRouteResult>;
560
+ optimize(stops: LngLat[], opts?: RoadRouteOptions): Promise<OptimizedRouteResult>;
171
561
  private post;
172
562
  private get;
173
563
  }
174
564
 
175
- export { type FeatureCollection, type IsochroneOptions, type LngLat, type MatrixResult, type OptimizedRouteResult, type RouteOptions, type RouteResult, Router, RouterError, type RouterOptions, type RoutingStatus, type TravelMode, type TruckProfile };
565
+ export { type AvoidAreaFeature, type AvoidAreaFeatureCollection, type AvoidAreaMultiPolygon, type AvoidAreaPolygon, type AvoidAreaPosition, type AvoidAreaRing, type AvoidAreas, type AvoidToken, type FeatureCollection, type IsochroneOptions, type LngLat, type MatrixResult, type OptimizedRouteResult, type PedestrianRouteLeg, type RoadRouteOptions, type RoadTravelMode, type RouteLeg, type RouteOptions, type RouteResult, type RouteWarning, Router, RouterError, type RouterOptions, type RoutingStatus, type TransitAgencySummary, type TransitRouteLeg, type TransitRouteOptions, type TransitRouteSummary, type TransitStopTime, type TravelMode, type TruckCoverage, type TruckMeta, type TruckProfile, type TruckValidation, type TruckValidationFinding, type TruckValidationStatus, type TruckVerification, type TruckWarning };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
- import { DEFAULT_GATEWAY } from "@unmap/core";
2
+ import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter } from "@unmap/core";
3
+ import { UnmapError as UnmapError2, parseRetryAfter as parseRetryAfter2 } from "@unmap/core";
3
4
  var TRUCK_PARAMS = {
4
5
  height: "height",
5
6
  width: "width",
@@ -31,20 +32,22 @@ function truckBody(truck) {
31
32
  function bodyOptions(opts) {
32
33
  return {
33
34
  ...opts.mode ? { mode: opts.mode } : {},
35
+ ...opts.departAt != null ? { depart_at: departureValue(opts.departAt) } : {},
36
+ ...opts.avoid?.length ? { avoid: opts.avoid } : {},
37
+ ...opts.avoidAreas != null ? { avoid_areas: opts.avoidAreas } : {},
34
38
  ...opts.useHighways != null ? { use_highways: opts.useHighways } : {},
35
39
  ...opts.useHills != null ? { use_hills: opts.useHills } : {},
36
40
  ...truckBody(opts.truck)
37
41
  };
38
42
  }
39
- var RouterError = class extends Error {
40
- constructor(status, message, code) {
41
- super(message);
42
- this.status = status;
43
- this.code = code;
43
+ function departureValue(value) {
44
+ return value instanceof Date ? value.toISOString() : value;
45
+ }
46
+ var RouterError = class extends UnmapError {
47
+ constructor(status, message, code, retryAfterSeconds) {
48
+ super(status, message, code, retryAfterSeconds);
44
49
  this.name = "RouterError";
45
50
  }
46
- status;
47
- code;
48
51
  };
49
52
  async function errorFrom(res, path) {
50
53
  const fallback = `route ${res.status} on ${path}`;
@@ -56,24 +59,32 @@ async function errorFrom(res, path) {
56
59
  }
57
60
  const reason = typeof body?.error === "string" ? body.error : void 0;
58
61
  const code = typeof body?.code === "string" ? body.code : void 0;
59
- return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code);
62
+ const retryAfter = parseRetryAfter(res.headers?.get("retry-after") ?? null) ?? void 0;
63
+ return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter);
60
64
  }
61
65
  var Router = class {
62
66
  key;
63
67
  base;
64
68
  fetchImpl;
69
+ retry;
65
70
  constructor(opts) {
71
+ this.retry = opts.retry;
66
72
  this.key = opts.key;
67
73
  this.base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\/+$/, "");
68
74
  this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
69
75
  }
70
76
  route(from, to, opts = {}) {
77
+ if (opts.avoidAreas != null) {
78
+ return this.post("/route", { from, to, ...bodyOptions(opts) }, opts.signal);
79
+ }
71
80
  return this.get(
72
81
  "/route",
73
82
  {
74
83
  from: from.join(","),
75
84
  to: to.join(","),
76
85
  mode: opts.mode,
86
+ depart_at: opts.departAt != null ? departureValue(opts.departAt) : void 0,
87
+ avoid: opts.avoid?.length ? opts.avoid.join(",") : void 0,
77
88
  use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
78
89
  use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
79
90
  ...truckParams(opts.truck)
@@ -82,6 +93,13 @@ var Router = class {
82
93
  );
83
94
  }
84
95
  isochrone(center, opts) {
96
+ if (opts.avoidAreas != null) {
97
+ return this.post(
98
+ "/isochrone",
99
+ { center, minutes: opts.minutes, ...bodyOptions(opts) },
100
+ opts.signal
101
+ );
102
+ }
85
103
  return this.get(
86
104
  "/isochrone",
87
105
  {
@@ -89,6 +107,7 @@ var Router = class {
89
107
  lat: String(center[1]),
90
108
  minutes: opts.minutes.join(","),
91
109
  mode: opts.mode,
110
+ avoid: opts.avoid?.length ? opts.avoid.join(",") : void 0,
92
111
  use_highways: opts.useHighways != null ? String(opts.useHighways) : void 0,
93
112
  use_hills: opts.useHills != null ? String(opts.useHills) : void 0,
94
113
  ...truckParams(opts.truck)
@@ -111,9 +130,13 @@ var Router = class {
111
130
  * Travel times and distances between every origin and every destination.
112
131
  *
113
132
  * At most 10 origins and 10 destinations. A `0` is a real answer, which is what you get
114
- * where an origin is also a destination. If any pair is unreachable, or beyond the engine's
115
- * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)
116
- * rather than resolving with a `null` cell for that pair.
133
+ * where an origin is also a destination, and a `null` is a real answer too: a pair the engine
134
+ * could not connect is nulled in place while the rest of the grid answers. A coordinate outside
135
+ * WGS84 rejects first with a `RouterError` (400, `invalid_location`). A location outside
136
+ * Canada rejects first with a `RouterError` (422, `outside_coverage`). The whole call otherwise
137
+ * rejects with a `RouterError` (422, `no_route`) only for a location with no road near it, a
138
+ * set of locations the road network does not connect, or two locations past its straight-line
139
+ * matrix limit.
117
140
  */
118
141
  matrix(origins, destinations, opts = {}) {
119
142
  return this.post("/matrix", { origins, destinations, ...bodyOptions(opts) }, opts.signal);
@@ -121,7 +144,9 @@ var Router = class {
121
144
  /**
122
145
  * Snap a GPS trace to the road network. At least 2 and at most 100 coordinates.
123
146
  *
124
- * Returns the same `RouteResult` `route()` returns: a matched trace is a route.
147
+ * Returns the same `RouteResult` `route()` returns: a matched trace is a route. A coordinate
148
+ * outside WGS84 rejects with a `RouterError` (400, `invalid_location`). A point outside Canada
149
+ * rejects with a `RouterError` (422, `outside_coverage`) before the graph is searched.
125
150
  */
126
151
  match(coordinates, opts = {}) {
127
152
  return this.post("/match", { coordinates, ...bodyOptions(opts) }, opts.signal);
@@ -133,38 +158,50 @@ var Router = class {
133
158
  * them are reordered. There is no round-trip or open-route toggle: that is a real constraint
134
159
  * of the underlying engine, not an omission here.
135
160
  *
136
- * Optimizing solves a cost matrix over every stop, so it is expected to fail the same way `matrix()` does:
137
- * if any stop is unreachable, or the set is spread further apart than the engine's own
138
- * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)
139
- * rather than resolving with a partial tour.
161
+ * Optimizing solves a cost matrix over every stop, but it does not inherit `matrix()`'s per-cell
162
+ * nulls: a stop set whose own matrix returns nulls still optimizes. A coordinate outside WGS84
163
+ * rejects first with a `RouterError` (400, `invalid_location`). A stop outside Canada rejects
164
+ * first with a `RouterError` (422, `outside_coverage`). The whole call otherwise rejects with
165
+ * a `RouterError` (422, `no_route`) when the stops are not all in one region the road network
166
+ * connects, or two are further apart than the engine's straight-line matrix limit. There is no
167
+ * partial tour either way, because a tour needs every leg.
140
168
  */
141
169
  optimize(stops, opts = {}) {
142
170
  return this.post("/optimized-route", { stops, ...bodyOptions(opts) }, opts.signal);
143
171
  }
144
172
  async post(path, body, signal) {
145
173
  const q = new URLSearchParams({ key: this.key });
146
- const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
147
- method: "POST",
148
- signal,
149
- headers: { accept: "application/json", "content-type": "application/json" },
150
- body: JSON.stringify(body)
151
- });
174
+ const res = await fetchWithRetry(
175
+ this.fetchImpl,
176
+ `${this.base}${path}?${q}`,
177
+ {
178
+ method: "POST",
179
+ signal,
180
+ headers: { accept: "application/json", "content-type": "application/json" },
181
+ body: JSON.stringify(body)
182
+ },
183
+ this.retry
184
+ );
152
185
  if (!res.ok) throw await errorFrom(res, path);
153
186
  return res.json();
154
187
  }
155
188
  async get(path, params, signal) {
156
189
  const q = new URLSearchParams({ key: this.key });
157
190
  for (const [k, v] of Object.entries(params)) if (v != null && v !== "") q.set(k, v);
158
- const res = await this.fetchImpl(`${this.base}${path}?${q}`, {
159
- signal,
160
- headers: { accept: "application/json" }
161
- });
191
+ const res = await fetchWithRetry(
192
+ this.fetchImpl,
193
+ `${this.base}${path}?${q}`,
194
+ { signal, headers: { accept: "application/json" } },
195
+ this.retry
196
+ );
162
197
  if (!res.ok) throw await errorFrom(res, path);
163
198
  return res.json();
164
199
  }
165
200
  };
166
201
  export {
167
202
  Router,
168
- RouterError
203
+ RouterError,
204
+ UnmapError2 as UnmapError,
205
+ parseRetryAfter2 as parseRetryAfter
169
206
  };
170
207
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { DEFAULT_GATEWAY } from \"@unmap/core\";\n\nexport interface RouterOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\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\";\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\nexport interface RouteOptions {\n mode?: TravelMode;\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 signal?: AbortSignal;\n}\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.useHighways != null ? { use_highways: opts.useHighways } : {}),\n ...(opts.useHills != null ? { use_hills: opts.useHills } : {}),\n ...truckBody(opts.truck),\n };\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\".\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}\n\nexport interface RouteResult {\n distanceMeters: number;\n durationSeconds: number;\n geometry: { type: \"LineString\"; coordinates: LngLat[] };\n}\n\n/**\n * Travel times and distances between every origin and every destination.\n *\n * If any pair is unreachable, or beyond the engine's own road-distance limit, `matrix()`\n * rejects with a `RouterError` (status 422, code `no_route`) instead of resolving with a\n * `null` cell for that pair: `null` never appears in a resolved `MatrixResult` in practice. It\n * stays in the type as a defensive case, costing nothing and remaining correct if a future\n * engine version starts nulling cells instead of failing the request.\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 RouteOptions {\n minutes: number[];\n}\n\nexport class RouterError extends Error {\n constructor(\n public readonly status: number,\n message: string,\n /** The gateway's machine-readable `code`, when the response carried one (e.g. `unauthorized`). */\n public readonly code?: string,\n ) {\n super(message);\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\", \"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; 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 return new RouterError(res.status, reason ? `${reason} (${fallback})` : fallback, code);\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\n constructor(opts: RouterOptions) {\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 return this.get<RouteResult>(\n \"/route\",\n {\n from: from.join(\",\"),\n to: to.join(\",\"),\n mode: opts.mode,\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 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 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. If any pair is unreachable, or beyond the engine's\n * own road-distance limit, the whole call rejects with a `RouterError` (422, `no_route`)\n * rather than resolving with a `null` cell for that pair.\n */\n matrix(origins: LngLat[], destinations: LngLat[], opts: RouteOptions = {}): 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.\n */\n match(coordinates: LngLat[], opts: RouteOptions = {}): 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, so it is expected to fail the same way `matrix()` does:\n * if any stop is unreachable, or the set is spread further apart than the engine's own\n * road-distance limit allows, the whole call rejects with a `RouterError` (422, `no_route`)\n * rather than resolving with a partial tour.\n */\n optimize(stops: LngLat[], opts: RouteOptions = {}): 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 this.fetchImpl(`${this.base}${path}?${q}`, {\n method: \"POST\",\n signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\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 this.fetchImpl(`${this.base}${path}?${q}`, {\n signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) throw await errorFrom(res, path);\n return res.json() as Promise<T>;\n }\n}\n"],"mappings":"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","names":[]}
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":"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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.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Typed client for the unmap routing API (route + isochrone)",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -11,7 +11,7 @@
11
11
  "valhalla",
12
12
  "canada"
13
13
  ],
14
- "homepage": "https://unmap.dev/docs/routing-api",
14
+ "homepage": "https://unmap.dev/docs/apis/routing",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/unmap-dev/unmap.git",
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@unmap/core": "0.12.0"
41
+ "@unmap/core": "0.14.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.3.0",