@unmap/routing 0.31.0 → 0.31.1

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/dist/index.d.ts CHANGED
@@ -21,7 +21,11 @@ type Timestamp = string | {
21
21
  seconds?: Int64;
22
22
  nanos?: number;
23
23
  };
24
- /** A 64-bit integer as a decimal string or a safe JSON integer. */
24
+ /**
25
+ * A 64-bit integer as a decimal string or a safe JSON integer. These types describe what a GMPRO
26
+ * client may send, so the string form stays; `validateRequest` returns every int64 field as a
27
+ * number, and code reading a validated request still goes through `parseInt64` to narrow it.
28
+ */
25
29
  type Int64 = string | number;
26
30
  type SolvingMode = "DEFAULT_SOLVE" | "VALIDATE_ONLY" | "DETECT_SOME_INFEASIBLE_SHIPMENTS" | "TRANSFORM_AND_RETURN_REQUEST";
27
31
  type SearchMode = "SEARCH_MODE_UNSPECIFIED" | "RETURN_FAST" | "CONSUME_ALL_AVAILABLE_TIME";
@@ -355,7 +359,10 @@ interface OptimizeToursResult {
355
359
  * is billed like any other.
356
360
  */
357
361
  billableShipments: number;
358
- /** True when your `timeout` was over the plan's cap and the gateway shortened it. */
362
+ /**
363
+ * True when your `timeout` was over the plan's cap and the gateway shortened it. The same fact
364
+ * also arrives as a `TIMEOUT_CLAMPED` entry in `warnings`; this field is the typed shortcut.
365
+ */
359
366
  timeoutClamped: boolean;
360
367
  /** The timeout the solve ran with, in seconds, when `timeoutClamped`; otherwise `null`. */
361
368
  clampedTimeoutSeconds: number | null;
@@ -1049,6 +1056,10 @@ declare class Router {
1049
1056
  * `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it
1050
1057
  * resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops
1051
1058
  * for a single vehicle, this one plans a fleet.
1059
+ *
1060
+ * Unlike the other methods, which put the key in the query string (`?key=`), this one sends it
1061
+ * in the `X-API-Key` header, as the standalone `optimizeTours()` does. The optimize paths also
1062
+ * accept Google's own `X-Goog-Api-Key`, so a GMPRO client keeps its header.
1052
1063
  */
1053
1064
  optimizeTours(request: OptimizeToursRequest, opts?: {
1054
1065
  signal?: AbortSignal;
package/dist/index.js CHANGED
@@ -78,7 +78,8 @@ async function optimizeTours(request, opts) {
78
78
  let response;
79
79
  try {
80
80
  response = await res.json();
81
- } catch {
81
+ } catch (error) {
82
+ if (opts.signal?.aborted) throw error;
82
83
  throw new UnmapOptimizeError(res.status, `optimize ${res.status} on /optimize: the response body is not JSON`);
83
84
  }
84
85
  const clamped = parseClamp(res.headers?.get("warning") ?? null);
@@ -270,6 +271,10 @@ var Router = class {
270
271
  * `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it
271
272
  * resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops
272
273
  * for a single vehicle, this one plans a fleet.
274
+ *
275
+ * Unlike the other methods, which put the key in the query string (`?key=`), this one sends it
276
+ * in the `X-API-Key` header, as the standalone `optimizeTours()` does. The optimize paths also
277
+ * accept Google's own `X-Goog-Api-Key`, so a GMPRO client keeps its header.
273
278
  */
274
279
  optimizeTours(request, opts = {}) {
275
280
  return optimizeTours(request, { key: this.key, gateway: this.base, fetch: this.fetchImpl, retry: this.retry, signal: opts.signal });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/optimize-tours.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport { optimizeTours, type OptimizeToursRequest, type OptimizeToursResult } from \"./optimize-tours\";\n\nexport * from \"./optimize-tours\";\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/** Turn-by-turn options, taken by `route()`, `match()` and `optimize()`. */\nexport interface StepOptions {\n /**\n * Return `steps`: written turn-by-turn directions for display. Off by default, so a route you\n * only draw stays small.\n */\n steps?: boolean;\n /**\n * Language of each step's `instruction`: `en` or `fr`. A regional tag resolves by its language\n * (`fr-CA` is French) and any other language is English. Only with `steps: true`; the gateway\n * rejects it otherwise. Without it the gateway reads the request's `Accept-Language` header,\n * which a browser sends for you.\n */\n lang?: string;\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 & StepOptions): Record<string, unknown> {\n return {\n ...(opts.steps != null ? { steps: opts.steps } : {}),\n ...(opts.lang != null ? { lang: opts.lang } : {}),\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 * Turn-by-turn directions, in travel order. Present only when you pass `steps: true`. On a\n * transit route they include the walk to and from each stop as well as the rides.\n */\n steps?: RouteStep[];\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 * What a step asks the traveller to do. unmap's own names, so your code does not depend on the\n * routing engine's internal numbering. Switch on this to choose an icon; show `instruction` as text.\n *\n * - `depart` / `arrive`: the first step of a route, and every stop it reaches (`/optimized-route`\n * arrives at each stop and departs again).\n * - `turn`, `uturn`, `continue`, `keep` (stay on a side at a split), `merge`, `name_change` (the\n * road you are on changes name).\n * - `ramp` (take a ramp) and `exit` (leave by an exit), both on limited-access roads.\n * - `roundabout` (enter; `roundaboutExit` says which exit) and `roundabout_exit`.\n * - `ferry` (board) and `ferry_exit`.\n * - Transit: `transit` (board), `transit_transfer`, `transit_stay_on`, `station_enter`,\n * `station_transfer`, `station_exit`.\n * - Inside buildings on a walk: `elevator`, `stairs`, `escalator`, `building_enter`, `building_exit`.\n * - `other`: a step the engine produced that unmap does not name yet. Show its instruction.\n */\nexport type RouteStepType =\n | \"depart\"\n | \"arrive\"\n | \"turn\"\n | \"uturn\"\n | \"continue\"\n | \"keep\"\n | \"merge\"\n | \"name_change\"\n | \"ramp\"\n | \"exit\"\n | \"roundabout\"\n | \"roundabout_exit\"\n | \"ferry\"\n | \"ferry_exit\"\n | \"transit\"\n | \"transit_transfer\"\n | \"transit_stay_on\"\n | \"station_enter\"\n | \"station_transfer\"\n | \"station_exit\"\n | \"elevator\"\n | \"stairs\"\n | \"escalator\"\n | \"building_enter\"\n | \"building_exit\"\n | \"other\";\n\n/**\n * Which way a step goes. Absent when the step has no direction (a roundabout, a transfer). On\n * `depart` and `arrive` it is the side of the road (`left`/`right`); on `uturn` the direction of\n * the turn.\n */\nexport type RouteStepModifier =\n | \"sharp_left\"\n | \"left\"\n | \"slight_left\"\n | \"straight\"\n | \"slight_right\"\n | \"right\"\n | \"sharp_right\";\n\n/** The guide sign at a highway junction, as posted. Every list is absent when the sign has none. */\nexport interface RouteStepSign {\n /** Exit numbers, e.g. `[\"91B\"]`. */\n exitNumbers?: string[];\n /** The road or route the exit leads onto, e.g. `[\"Highway 2 North\"]`. */\n branches?: string[];\n /** Where the exit goes, e.g. `[\"Red Deer\"]`. */\n toward?: string[];\n /** The interchange's own name. Rare in Canada. */\n names?: string[];\n}\n\n/**\n * One turn-by-turn step: written directions, for display. There is no voice narration.\n *\n * Present on a route only when you ask for `steps`. Steps cover the whole route in travel order, so\n * their distances and durations add up to the route's, give or take rounding.\n */\nexport interface RouteStep {\n type: RouteStepType;\n modifier?: RouteStepModifier;\n /**\n * The step as a sentence, e.g. \"Turn left onto 17 Avenue Southwest.\", in the language you asked\n * for (`en` or `fr`). French is written by the engine's France French locale, not a Canadian\n * one, so a few phrasings read as European French.\n */\n instruction: string;\n /** Names of the road the step travels along. Absent on an unnamed road. */\n streetNames?: string[];\n distanceMeters: number;\n durationSeconds: number;\n /** Where the step begins, `[lng, lat]`. The same point as `geometry.coordinates[geometryRange[0]]`. */\n location: LngLat;\n /**\n * First and last index of this step's stretch in the route's `geometry.coordinates`, inclusive.\n * `coordinates.slice(start, end + 1)` is the line to highlight for the step.\n */\n geometryRange: [number, number];\n /** On `type: \"roundabout\"`, which exit to take, counting from where you enter. */\n roundaboutExit?: number;\n sign?: RouteStepSign;\n}\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 & StepOptions = {}): 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 steps: opts.steps != null ? String(opts.steps) : undefined,\n lang: opts.lang,\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 & StepOptions = {}): 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 & StepOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Assign shipments to vehicles and order their visits: Google Route Optimization's\n * `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it\n * resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops\n * for a single vehicle, this one plans a fleet.\n */\n optimizeTours(request: OptimizeToursRequest, opts: { signal?: AbortSignal } = {}): Promise<OptimizeToursResult> {\n return optimizeTours(request, { key: this.key, gateway: this.base, fetch: this.fetchImpl, retry: this.retry, signal: 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","import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport type { OptimizeToursRequest, OptimizeToursResponse } from \"@unmap/optimize-schema\";\n\nexport type {\n OptimizeToursRequest,\n OptimizeToursResponse,\n ShipmentModel as OptimizeShipmentModel,\n Shipment as OptimizeShipment,\n Vehicle as OptimizeVehicle,\n ShipmentRoute as OptimizeShipmentRoute,\n SkippedShipment as OptimizeSkippedShipment,\n} from \"@unmap/optimize-schema\";\n\nexport interface OptimizeToursOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it. A failed\n * solve is never billed, so a retry costs nothing, but a solve that is still running when the\n * connection drops is not cancelled: it holds one of your plan's concurrent solves until it ends.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\n signal?: AbortSignal;\n}\n\n/**\n * One entry of the `X-Unmap-Optimize-Warnings` header: a field Unmap accepted but cannot honour,\n * a clamped timeout, road travel the routing budget decided, or a route polyline that could not be\n * drawn. A final `WARNINGS_TRUNCATED` entry counts any that did not fit the header.\n */\nexport interface OptimizeToursWarning {\n code: string;\n field?: string;\n path?: string;\n count?: number;\n message: string;\n}\n\n/**\n * What `optimizeTours()` resolves with.\n *\n * `response` is Google's `OptimizeToursResponse`, exactly as the gateway sent it: Unmap never adds\n * a field to it, so code written against Google's response shape reads `response` unchanged. What\n * Unmap knows beyond that arrives in the response headers and is parsed here, beside it.\n */\nexport interface OptimizeToursResult {\n response: OptimizeToursResponse;\n /** Parsed `X-Unmap-Optimize-Warnings`. Empty when there were none or the header was unreadable. */\n warnings: OptimizeToursWarning[];\n /**\n * Parsed `X-Unmap-Billable-Shipments`: the shipments this request billed. `0` for\n * `VALIDATE_ONLY`, a free re-solve within 24 hours, and when the header is absent. A dev origin\n * is billed like any other.\n */\n billableShipments: number;\n /** True when your `timeout` was over the plan's cap and the gateway shortened it. */\n timeoutClamped: boolean;\n /** The timeout the solve ran with, in seconds, when `timeoutClamped`; otherwise `null`. */\n clampedTimeoutSeconds: number | null;\n}\n\n/** One Google `Status.details` entry (`google.rpc.BadRequest`, `PreconditionFailure`, `RetryInfo`, ...). */\nexport interface OptimizeToursErrorDetail {\n \"@type\"?: string;\n [key: string]: unknown;\n}\n\n/**\n * An Unmap Optimize failure.\n *\n * Extends `UnmapError`, so `instanceof UnmapError` catches it with every other unmap client's\n * errors. `status` is the HTTP status, `code` the gateway's string code (`bad_request`,\n * `rate_limited`, `quota_exceeded`, ...), and `retryAfterSeconds` comes from `Retry-After`.\n * `googleStatus` is Google's RPC status name (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, ...) and\n * `details` Google's `error.details`: a `google.rpc.BadRequest` lists each field violation, and a\n * `PreconditionFailure` lists the mandatory shipments no vehicle can serve. A response that is\n * not Google-shaped (the gateway's plain `404` when Optimize is not enabled) has no\n * `googleStatus` and an empty `details`.\n */\nexport class UnmapOptimizeError extends UnmapError {\n constructor(\n status: number,\n message: string,\n code?: string,\n retryAfterSeconds?: number,\n readonly googleStatus?: string,\n readonly details: OptimizeToursErrorDetail[] = [],\n ) {\n super(status, message, code, retryAfterSeconds);\n this.name = \"UnmapOptimizeError\";\n }\n}\n\nasync function optimizeErrorFrom(res: { status: number; headers?: Headers; json(): Promise<unknown> }): Promise<UnmapOptimizeError> {\n const fallback = `optimize ${res.status} on /optimize`;\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n body = undefined;\n }\n const top = (typeof body === \"object\" && body !== null ? body : {}) as { error?: unknown; code?: unknown };\n const code = typeof top.code === \"string\" ? top.code : undefined;\n const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n\n let reason: string | undefined;\n let googleStatus: string | undefined;\n let details: OptimizeToursErrorDetail[] = [];\n if (typeof top.error === \"string\") {\n reason = top.error;\n } else if (typeof top.error === \"object\" && top.error !== null) {\n const g = top.error as { message?: unknown; status?: unknown; details?: unknown };\n if (typeof g.message === \"string\") reason = g.message;\n if (typeof g.status === \"string\") googleStatus = g.status;\n if (Array.isArray(g.details)) {\n details = g.details.filter((d): d is OptimizeToursErrorDetail => typeof d === \"object\" && d !== null);\n }\n }\n return new UnmapOptimizeError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter, googleStatus, details);\n}\n\nfunction parseWarnings(header: string | null): OptimizeToursWarning[] {\n if (!header) return [];\n try {\n const parsed: unknown = JSON.parse(header);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (w): w is OptimizeToursWarning =>\n typeof w === \"object\" && w !== null && typeof (w as { code?: unknown }).code === \"string\" && typeof (w as { message?: unknown }).message === \"string\",\n );\n } catch {\n return [];\n }\n}\n\nfunction parseBillable(header: string | null): number {\n if (!header || !/^[0-9]+$/.test(header.trim())) return 0;\n return Number(header.trim());\n}\n\nfunction parseClamp(header: string | null): number | null {\n const m = header ? /timeout clamped to (\\d+)s/.exec(header) : null;\n return m ? Number(m[1]) : null;\n}\n\n/**\n * Assign shipments to vehicles and order their visits (Unmap Optimize, `POST /optimize`).\n *\n * The request and the `response` are Google Route Optimization's `OptimizeToursRequest` and\n * `OptimizeToursResponse`. Unmap's own findings (warnings, the shipments billed) are returned\n * beside the response, never inside it.\n *\n * const { response } = await optimizeTours(request, { key: \"um_live_...\" });\n *\n * The key is sent in `X-API-Key`, not in the URL. Billed per shipment performed; see\n * `billableShipments`. A failure throws `UnmapOptimizeError`.\n */\nexport async function optimizeTours(request: OptimizeToursRequest, opts: OptimizeToursOptions): Promise<OptimizeToursResult> {\n const base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n const path = \"/optimize\";\n // Bound to globalThis: a bare `fetch` stored on an object is rejected by browsers with\n // \"Illegal invocation\" when called with that object as the receiver.\n const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n const res = await fetchWithRetry(\n fetchImpl,\n `${base}${path}`,\n {\n method: \"POST\",\n signal: opts.signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\", \"x-api-key\": opts.key },\n body: JSON.stringify(request),\n },\n opts.retry,\n );\n if (!res.ok) throw await optimizeErrorFrom(res);\n let response: OptimizeToursResponse;\n try {\n response = (await res.json()) as OptimizeToursResponse;\n } catch {\n // A proxy or a captive portal can answer 200 with HTML; callers catch one error type.\n throw new UnmapOptimizeError(res.status, `optimize ${res.status} on /optimize: the response body is not JSON`);\n }\n const clamped = parseClamp(res.headers?.get(\"warning\") ?? null);\n return {\n response,\n warnings: parseWarnings(res.headers?.get(\"x-unmap-optimize-warnings\") ?? null),\n billableShipments: parseBillable(res.headers?.get(\"x-unmap-billable-shipments\") ?? null),\n timeoutClamped: clamped !== null,\n clampedTimeoutSeconds: clamped,\n };\n}\n"],"mappings":"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","names":["DEFAULT_GATEWAY","UnmapError","fetchWithRetry","parseRetryAfter","UnmapError","parseRetryAfter","DEFAULT_GATEWAY","fetchWithRetry"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/optimize-tours.ts"],"sourcesContent":["import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport { optimizeTours, type OptimizeToursRequest, type OptimizeToursResult } from \"./optimize-tours\";\n\nexport * from \"./optimize-tours\";\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/** Turn-by-turn options, taken by `route()`, `match()` and `optimize()`. */\nexport interface StepOptions {\n /**\n * Return `steps`: written turn-by-turn directions for display. Off by default, so a route you\n * only draw stays small.\n */\n steps?: boolean;\n /**\n * Language of each step's `instruction`: `en` or `fr`. A regional tag resolves by its language\n * (`fr-CA` is French) and any other language is English. Only with `steps: true`; the gateway\n * rejects it otherwise. Without it the gateway reads the request's `Accept-Language` header,\n * which a browser sends for you.\n */\n lang?: string;\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 & StepOptions): Record<string, unknown> {\n return {\n ...(opts.steps != null ? { steps: opts.steps } : {}),\n ...(opts.lang != null ? { lang: opts.lang } : {}),\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 * Turn-by-turn directions, in travel order. Present only when you pass `steps: true`. On a\n * transit route they include the walk to and from each stop as well as the rides.\n */\n steps?: RouteStep[];\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 * What a step asks the traveller to do. unmap's own names, so your code does not depend on the\n * routing engine's internal numbering. Switch on this to choose an icon; show `instruction` as text.\n *\n * - `depart` / `arrive`: the first step of a route, and every stop it reaches (`/optimized-route`\n * arrives at each stop and departs again).\n * - `turn`, `uturn`, `continue`, `keep` (stay on a side at a split), `merge`, `name_change` (the\n * road you are on changes name).\n * - `ramp` (take a ramp) and `exit` (leave by an exit), both on limited-access roads.\n * - `roundabout` (enter; `roundaboutExit` says which exit) and `roundabout_exit`.\n * - `ferry` (board) and `ferry_exit`.\n * - Transit: `transit` (board), `transit_transfer`, `transit_stay_on`, `station_enter`,\n * `station_transfer`, `station_exit`.\n * - Inside buildings on a walk: `elevator`, `stairs`, `escalator`, `building_enter`, `building_exit`.\n * - `other`: a step the engine produced that unmap does not name yet. Show its instruction.\n */\nexport type RouteStepType =\n | \"depart\"\n | \"arrive\"\n | \"turn\"\n | \"uturn\"\n | \"continue\"\n | \"keep\"\n | \"merge\"\n | \"name_change\"\n | \"ramp\"\n | \"exit\"\n | \"roundabout\"\n | \"roundabout_exit\"\n | \"ferry\"\n | \"ferry_exit\"\n | \"transit\"\n | \"transit_transfer\"\n | \"transit_stay_on\"\n | \"station_enter\"\n | \"station_transfer\"\n | \"station_exit\"\n | \"elevator\"\n | \"stairs\"\n | \"escalator\"\n | \"building_enter\"\n | \"building_exit\"\n | \"other\";\n\n/**\n * Which way a step goes. Absent when the step has no direction (a roundabout, a transfer). On\n * `depart` and `arrive` it is the side of the road (`left`/`right`); on `uturn` the direction of\n * the turn.\n */\nexport type RouteStepModifier =\n | \"sharp_left\"\n | \"left\"\n | \"slight_left\"\n | \"straight\"\n | \"slight_right\"\n | \"right\"\n | \"sharp_right\";\n\n/** The guide sign at a highway junction, as posted. Every list is absent when the sign has none. */\nexport interface RouteStepSign {\n /** Exit numbers, e.g. `[\"91B\"]`. */\n exitNumbers?: string[];\n /** The road or route the exit leads onto, e.g. `[\"Highway 2 North\"]`. */\n branches?: string[];\n /** Where the exit goes, e.g. `[\"Red Deer\"]`. */\n toward?: string[];\n /** The interchange's own name. Rare in Canada. */\n names?: string[];\n}\n\n/**\n * One turn-by-turn step: written directions, for display. There is no voice narration.\n *\n * Present on a route only when you ask for `steps`. Steps cover the whole route in travel order, so\n * their distances and durations add up to the route's, give or take rounding.\n */\nexport interface RouteStep {\n type: RouteStepType;\n modifier?: RouteStepModifier;\n /**\n * The step as a sentence, e.g. \"Turn left onto 17 Avenue Southwest.\", in the language you asked\n * for (`en` or `fr`). French is written by the engine's France French locale, not a Canadian\n * one, so a few phrasings read as European French.\n */\n instruction: string;\n /** Names of the road the step travels along. Absent on an unnamed road. */\n streetNames?: string[];\n distanceMeters: number;\n durationSeconds: number;\n /** Where the step begins, `[lng, lat]`. The same point as `geometry.coordinates[geometryRange[0]]`. */\n location: LngLat;\n /**\n * First and last index of this step's stretch in the route's `geometry.coordinates`, inclusive.\n * `coordinates.slice(start, end + 1)` is the line to highlight for the step.\n */\n geometryRange: [number, number];\n /** On `type: \"roundabout\"`, which exit to take, counting from where you enter. */\n roundaboutExit?: number;\n sign?: RouteStepSign;\n}\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 & StepOptions = {}): 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 steps: opts.steps != null ? String(opts.steps) : undefined,\n lang: opts.lang,\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 & StepOptions = {}): 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 & StepOptions = {}): Promise<OptimizedRouteResult> {\n return this.post<OptimizedRouteResult>(\"/optimized-route\", { stops, ...bodyOptions(opts) }, opts.signal);\n }\n\n /**\n * Assign shipments to vehicles and order their visits: Google Route Optimization's\n * `optimizeTours`, served by Unmap Optimize. See the standalone `optimizeTours()` for what it\n * resolves with and throws. This is `/optimized-route`'s bigger sibling: that one orders stops\n * for a single vehicle, this one plans a fleet.\n *\n * Unlike the other methods, which put the key in the query string (`?key=`), this one sends it\n * in the `X-API-Key` header, as the standalone `optimizeTours()` does. The optimize paths also\n * accept Google's own `X-Goog-Api-Key`, so a GMPRO client keeps its header.\n */\n optimizeTours(request: OptimizeToursRequest, opts: { signal?: AbortSignal } = {}): Promise<OptimizeToursResult> {\n return optimizeTours(request, { key: this.key, gateway: this.base, fetch: this.fetchImpl, retry: this.retry, signal: 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","import { DEFAULT_GATEWAY, UnmapError, fetchWithRetry, parseRetryAfter, type RetryPolicy } from \"@unmap/core\";\nimport type { OptimizeToursRequest, OptimizeToursResponse } from \"@unmap/optimize-schema\";\n\nexport type {\n OptimizeToursRequest,\n OptimizeToursResponse,\n ShipmentModel as OptimizeShipmentModel,\n Shipment as OptimizeShipment,\n Vehicle as OptimizeVehicle,\n ShipmentRoute as OptimizeShipmentRoute,\n SkippedShipment as OptimizeSkippedShipment,\n} from \"@unmap/optimize-schema\";\n\nexport interface OptimizeToursOptions {\n key: string;\n gateway?: string;\n fetch?: typeof fetch;\n /**\n * Retry policy for `429`, `502`, `503` and transport failures. `false` disables it. A failed\n * solve is never billed, so a retry costs nothing, but a solve that is still running when the\n * connection drops is not cancelled: it holds one of your plan's concurrent solves until it ends.\n * See `RetryPolicy` in `@unmap/core`.\n */\n retry?: RetryPolicy | false;\n signal?: AbortSignal;\n}\n\n/**\n * One entry of the `X-Unmap-Optimize-Warnings` header: a field Unmap accepted but cannot honour,\n * a clamped timeout, road travel the routing budget decided, or a route polyline that could not be\n * drawn. A final `WARNINGS_TRUNCATED` entry counts any that did not fit the header.\n */\nexport interface OptimizeToursWarning {\n code: string;\n field?: string;\n path?: string;\n count?: number;\n message: string;\n}\n\n/**\n * What `optimizeTours()` resolves with.\n *\n * `response` is Google's `OptimizeToursResponse`, exactly as the gateway sent it: Unmap never adds\n * a field to it, so code written against Google's response shape reads `response` unchanged. What\n * Unmap knows beyond that arrives in the response headers and is parsed here, beside it.\n */\nexport interface OptimizeToursResult {\n response: OptimizeToursResponse;\n /** Parsed `X-Unmap-Optimize-Warnings`. Empty when there were none or the header was unreadable. */\n warnings: OptimizeToursWarning[];\n /**\n * Parsed `X-Unmap-Billable-Shipments`: the shipments this request billed. `0` for\n * `VALIDATE_ONLY`, a free re-solve within 24 hours, and when the header is absent. A dev origin\n * is billed like any other.\n */\n billableShipments: number;\n /**\n * True when your `timeout` was over the plan's cap and the gateway shortened it. The same fact\n * also arrives as a `TIMEOUT_CLAMPED` entry in `warnings`; this field is the typed shortcut.\n */\n timeoutClamped: boolean;\n /** The timeout the solve ran with, in seconds, when `timeoutClamped`; otherwise `null`. */\n clampedTimeoutSeconds: number | null;\n}\n\n/** One Google `Status.details` entry (`google.rpc.BadRequest`, `PreconditionFailure`, `RetryInfo`, ...). */\nexport interface OptimizeToursErrorDetail {\n \"@type\"?: string;\n [key: string]: unknown;\n}\n\n/**\n * An Unmap Optimize failure.\n *\n * Extends `UnmapError`, so `instanceof UnmapError` catches it with every other unmap client's\n * errors. `status` is the HTTP status, `code` the gateway's string code (`bad_request`,\n * `rate_limited`, `quota_exceeded`, ...), and `retryAfterSeconds` comes from `Retry-After`.\n * `googleStatus` is Google's RPC status name (`INVALID_ARGUMENT`, `FAILED_PRECONDITION`, ...) and\n * `details` Google's `error.details`: a `google.rpc.BadRequest` lists each field violation, and a\n * `PreconditionFailure` lists the mandatory shipments no vehicle can serve. A response that is\n * not Google-shaped (the gateway's plain `404` when Optimize is not enabled) has no\n * `googleStatus` and an empty `details`.\n */\nexport class UnmapOptimizeError extends UnmapError {\n constructor(\n status: number,\n message: string,\n code?: string,\n retryAfterSeconds?: number,\n readonly googleStatus?: string,\n readonly details: OptimizeToursErrorDetail[] = [],\n ) {\n super(status, message, code, retryAfterSeconds);\n this.name = \"UnmapOptimizeError\";\n }\n}\n\nasync function optimizeErrorFrom(res: { status: number; headers?: Headers; json(): Promise<unknown> }): Promise<UnmapOptimizeError> {\n const fallback = `optimize ${res.status} on /optimize`;\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n body = undefined;\n }\n const top = (typeof body === \"object\" && body !== null ? body : {}) as { error?: unknown; code?: unknown };\n const code = typeof top.code === \"string\" ? top.code : undefined;\n const retryAfter = parseRetryAfter(res.headers?.get(\"retry-after\") ?? null) ?? undefined;\n\n let reason: string | undefined;\n let googleStatus: string | undefined;\n let details: OptimizeToursErrorDetail[] = [];\n if (typeof top.error === \"string\") {\n reason = top.error;\n } else if (typeof top.error === \"object\" && top.error !== null) {\n const g = top.error as { message?: unknown; status?: unknown; details?: unknown };\n if (typeof g.message === \"string\") reason = g.message;\n if (typeof g.status === \"string\") googleStatus = g.status;\n if (Array.isArray(g.details)) {\n details = g.details.filter((d): d is OptimizeToursErrorDetail => typeof d === \"object\" && d !== null);\n }\n }\n return new UnmapOptimizeError(res.status, reason ? `${reason} (${fallback})` : fallback, code, retryAfter, googleStatus, details);\n}\n\nfunction parseWarnings(header: string | null): OptimizeToursWarning[] {\n if (!header) return [];\n try {\n const parsed: unknown = JSON.parse(header);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(\n (w): w is OptimizeToursWarning =>\n typeof w === \"object\" && w !== null && typeof (w as { code?: unknown }).code === \"string\" && typeof (w as { message?: unknown }).message === \"string\",\n );\n } catch {\n return [];\n }\n}\n\nfunction parseBillable(header: string | null): number {\n if (!header || !/^[0-9]+$/.test(header.trim())) return 0;\n return Number(header.trim());\n}\n\nfunction parseClamp(header: string | null): number | null {\n const m = header ? /timeout clamped to (\\d+)s/.exec(header) : null;\n return m ? Number(m[1]) : null;\n}\n\n/**\n * Assign shipments to vehicles and order their visits (Unmap Optimize, `POST /optimize`).\n *\n * The request and the `response` are Google Route Optimization's `OptimizeToursRequest` and\n * `OptimizeToursResponse`. Unmap's own findings (warnings, the shipments billed) are returned\n * beside the response, never inside it.\n *\n * const { response } = await optimizeTours(request, { key: \"um_live_...\" });\n *\n * The key is sent in `X-API-Key`, not in the URL. Billed per shipment performed; see\n * `billableShipments`. A failure throws `UnmapOptimizeError`.\n */\nexport async function optimizeTours(request: OptimizeToursRequest, opts: OptimizeToursOptions): Promise<OptimizeToursResult> {\n const base = (opts.gateway ?? DEFAULT_GATEWAY).replace(/\\/+$/, \"\");\n const path = \"/optimize\";\n // Bound to globalThis: a bare `fetch` stored on an object is rejected by browsers with\n // \"Illegal invocation\" when called with that object as the receiver.\n const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);\n const res = await fetchWithRetry(\n fetchImpl,\n `${base}${path}`,\n {\n method: \"POST\",\n signal: opts.signal,\n headers: { accept: \"application/json\", \"content-type\": \"application/json\", \"x-api-key\": opts.key },\n body: JSON.stringify(request),\n },\n opts.retry,\n );\n if (!res.ok) throw await optimizeErrorFrom(res);\n let response: OptimizeToursResponse;\n try {\n response = (await res.json()) as OptimizeToursResponse;\n } catch (error) {\n // Aborted while the body was arriving: the caller's own cancellation, not a bad body.\n if (opts.signal?.aborted) throw error;\n // A proxy or a captive portal can answer 200 with HTML; callers catch one error type.\n throw new UnmapOptimizeError(res.status, `optimize ${res.status} on /optimize: the response body is not JSON`);\n }\n const clamped = parseClamp(res.headers?.get(\"warning\") ?? null);\n return {\n response,\n warnings: parseWarnings(res.headers?.get(\"x-unmap-optimize-warnings\") ?? null),\n billableShipments: parseBillable(res.headers?.get(\"x-unmap-billable-shipments\") ?? null),\n timeoutClamped: clamped !== null,\n clampedTimeoutSeconds: clamped,\n };\n}\n"],"mappings":"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","names":["DEFAULT_GATEWAY","UnmapError","fetchWithRetry","parseRetryAfter","UnmapError","parseRetryAfter","DEFAULT_GATEWAY","fetchWithRetry"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unmap/routing",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
4
4
  "description": "Typed client for the unmap routing API (route + isochrone)",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -38,13 +38,13 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@unmap/core": "0.31.0"
41
+ "@unmap/core": "0.31.1"
42
42
  },
43
43
  "devDependencies": {
44
44
  "tsup": "^8.3.0",
45
45
  "typescript": "^5.6.0",
46
- "vitest": "^2.1.0",
47
- "@unmap/optimize-schema": "0.31.0"
46
+ "vitest": "^4.1.12",
47
+ "@unmap/optimize-schema": "0.31.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsup",