Skip to content

JS SDK лавлах ​

@dock/js-ийн экспорт бүр, TypeScript хөрвүүлэгчийн зарласнаар, тайлбартайгаа: багцын dist/*.d.ts-тэй ижил текст, эх файл бүрт нэг блок. Тайлбар нь кодонд бичигдсэн тул англиар. sdk/js/src-ээс scripts/sdk-reference.ts бичнэ; энэ хуудас кодоос хоцорсон, эсвэл экспорт тайлбаргүй бол make test-docs унана. Дуудлага бүрийг жишээтэй нь: JS SDK.

index.ts ​

ts
/** This SDK version, as in package.json. */
export const VERSION = "0.82.0";

client.ts ​

ts
/** The dock API, used when ClientOptions has no baseUrl. */
export const DEFAULT_BASE_URL = "https://geo.dock.mn";

/** Same shape as the API's key check (api/src/modules/auth/api-key.ts). */
export const KEY_RE: RegExp;

/** Ends per side in one matrix call, as the API's MATRIX_MAX (api/src/modules/route/route-input.ts). */
export const MATRIX_MAX = 10;

/** Contours in one isochrone call, as the API's CONTOURS_MAX (api/src/modules/route/route-input.ts). */
export const ISOCHRONE_CONTOURS_MAX = 4;

/** The longest contour in minutes, as the API's CONTOUR_MINUTES_MAX. */
export const ISOCHRONE_MINUTES_MAX = 60;

/** Points in one matching call, as the API's TRACE_MAX (api/src/modules/route/route-input.ts); at least 2. */
export const MATCHING_MAX = 100;

/** Ends a route passes between from and to, as the API's VIA_MAX (api/src/modules/route/route-input.ts). */
export const ROUTE_VIA_MAX = 10;

/** Stops in one optimizedTrips call, as the API's STOPS_MAX (api/src/modules/route/route-input.ts); at least 2. */
export const OPTIMIZED_TRIPS_MAX = 12;

/** The widest tilequery radius in metres, as the API's TILEQUERY_RADIUS_MAX_M (api/src/modules/tiles/tilequery.ts). */
export const TILEQUERY_RADIUS_MAX_M = 1000;

/** Features in one tilequery answer, as the API's TILEQUERY_LIMIT_MAX; 5 when no limit is sent. */
export const TILEQUERY_LIMIT_MAX = 50;

/** The kinds reverse can keep, as the API's REVERSE_KINDS (api/src/modules/search/search-store.ts). */
export const REVERSE_TYPES: readonly ReverseType[];

/** The kinds geocode can keep, as the API's PLACE_KINDS (api/src/modules/search/search.dto.ts). */
export const GEOCODE_TYPES: readonly PlaceKind[];

/** The categories nearby takes, as the API's NEARBY_CATEGORIES (api/src/modules/search/search.dto.ts). */
export const NEARBY_CATEGORIES: readonly NearbyCategory[];

/** The fetch the client calls: globalThis.fetch by default, your own for tests or old runtimes. */
export type Fetch = (input: string, init?: RequestInit) => Promise<Response>;

/** What new Client() takes. Only key is required. */
export interface ClientOptions {
  /** dk_ and 32 characters. Lives here only; every call and every tile takes it from here. */
  key: string;
  /** Default https://geo.dock.mn. */
  baseUrl?: string;
  /** For tests or old runtimes. Default globalThis.fetch. */
  fetch?: Fetch;
  /**
   * Called once per route when an answer says the route is deprecated. Default: one console.warn per route.
   * Pass a function to send it to your own logs, or `() => {}` to stay quiet.
   */
  onDeprecation?: (notice: DeprecationNotice) => void;
  /**
   * Share geocode searches and picks with dock (PLAN.md 18). Default false. Set true only after the user opted in;
   * setTelemetry() changes it later. Sent: the typed text, the result count, the pick. Not sent: time, point,
   * device, session.
   */
  telemetry?: boolean;
  /**
   * Places the user picked, kept on this device so geocode still answers when the network is down (PLAN.md 18).
   * Default on, in memory, 100 places. Pass { store: localStorage } to keep them across page loads, false for none.
   * Nothing here is sent anywhere.
   */
  placeCache?: PlaceCacheOptions | false;
}

/** A route the API marks deprecated (Deprecation, Sunset and Link headers, PLAN.md 17). */
export interface DeprecationNotice {
  /** "GET /v1/door/:code", the route, not the URL: no codes, no query. */
  route: string;
  /** Day it was announced, YYYY-MM-DD Ulaanbaatar, or null if the header did not parse. */
  deprecated: string | null;
  /** Day the route stops answering (410 sunset from 00:00 Ulaanbaatar), YYYY-MM-DD, or null. */
  sunset: string | null;
  /** The status page with the replacement route, or null. */
  link: string | null;
}

/** The Idempotency-Key format: 1 to 64 of letters, digits and ._:- */
export const IDEMPOTENCY_KEY_RE: RegExp;

/** Options every call takes. signal aborts the request. */
export interface CallOptions {
  signal?: AbortSignal;
}

/** What startTrace() takes. */
export interface TraceOptions {
  /** Must be 'opt_in': start only after the user said yes to sharing this trip. */
  consent: 'opt_in';
  /** Default navigator.geolocation. A native bridge or a test can pass its own. */
  source?: PositionSource;
  /** Fixes less accurate than this, in meters, are left out. Default 50. Accuracy is never sent. */
  maxAccuracyM?: number;
}

/** What startProbes() takes. */
export interface ProbeOptions {
  /** Default navigator.geolocation. A native bridge or a test can pass its own. */
  source?: PositionSource;
  /** Fixes less accurate than this, in meters, are left out. Default 50. Accuracy is never sent. */
  maxAccuracyM?: number;
  /** Seconds between sends, 5 or more. Default 30. */
  intervalS?: number;
  /** A failed send, the batch dropped: probes are live, a late one is not counted. On feature_off probes stop. */
  onError?: (err: DockError) => void;
}

/** What startEventPositions() takes. */
export interface EventPositionOptions {
  /** Must be 'opt_in': start only after the participant said yes to sharing their position with the organizer. */
  consent: 'opt_in';
  /** Default navigator.geolocation. A native bridge or a test can pass its own. */
  source?: PositionSource;
  /** Fixes less accurate than this, in meters, are left out. Default 50. Accuracy is never sent. */
  maxAccuracyM?: number;
  /** Seconds between sends, 5 or more. Default 10. */
  intervalS?: number;
  /** A failed send, the batch dropped. On event_closed, not_found or feature_off sending stops. */
  onError?: (err: DockError) => void;
}

/**
 * An API refusal or a failed request. `code` is the API's `error` field (invalid_code, not_found,
 * rate_limited, quota_exceeded, miss_cap, ...), or `network` when no response came back.
 */
export class DockError extends Error {
  /** HTTP status, 0 when no response came back. */
  readonly status: number;
  /** The API's error field, or network. */
  readonly code: string;
  /** Seconds, from Retry-After on 429 and 503. null otherwise. */
  readonly retryAfter: number | null;
  constructor(status: number, code: string, message: string, retryAfter?: number | null);
}

/** Shape MapLibre's transformRequest returns. */
export interface RequestParameters {
  url: string;
  headers?: Record<string, string>;
}

/**
 * The dock API v1. JSON calls send the key in the Authorization header. Map requests to this API
 * (TileJSON, tiles) get ?access_token= through transformRequest: one request per tile, no CORS preflight,
 * and the API never logs query strings.
 */
export class Client {
  /** The API this client calls, without a trailing slash. */
  readonly baseUrl: string;
  constructor(options: ClientOptions);
  /**
   * Name or address to places, best first. Needs geocode:read. limit 1 to 20. With no network (DockError code
   * network) the answer comes from places the user picked on this device, with `offline: true`, score = times picked,
   * fields as last seen; with none of them matching, the network error is thrown. `proximity` is [lon, lat], where the
   * person is or the map centre: among names the query starts equally well, nearer places come first. It is sent with
   * this request only, never kept or sent with telemetry; offline answers do not read it. `types` (API 0.70.0, Mapbox's
   * types): only these kinds, chosen before the limit, offline too; sent once each, in the order given. Absent or
   * empty: every kind. Another kind is refused here with the API's 400 invalid_types: no request.
   */
  geocode(q: string, options?: CallOptions & {
    limit?: number;
    proximity?: LonLat;
    types?: PlaceKind[];
  }): Promise<GeocodeAnswer>;
  /**
   * The user's consent changed. Off drops every search not yet sent. On, searches go in batches of 50 and when the
   * page is hidden (tab closed, app switched).
   */
  setTelemetry(on: boolean): void;
  /**
   * The user chose this result from a geocode list. It is kept on this device for offline search (placeCache), and
   * with telemetry on it closes the search sent to dock.
   */
  pick(result: GeocodeResult): void;
  /** Places the user picked on this device, most picked first: a "your places" list. [] with placeCache false. */
  places(): Place[];
  /** The user asked to forget their places: clears memory and the store. */
  forgetPlaces(): void;
  /**
   * Sends what is waiting now, for example when an order is placed. null when telemetry is off or nothing waits.
   * A failed send rejects and its searches are not kept: no retry, nothing stored in the browser.
   */
  flushTelemetry(): Promise<SearchTelemetryReceipt | null>;
  /**
   * Starts recording the user's trip (PLAN.md 15). Off until called; call it only after the user said yes. Points
   * stay in memory, never in browser storage. Throws when consent is not 'opt_in', when a trip is already being
   * recorded, or when there is no geolocation.
   */
  startTrace(options: TraceOptions): void;
  /** true while a trip is being recorded. */
  get tracing(): boolean;
  /**
   * Stops recording and sends the trip to POST /v1/traces, split every 20,000 points. Needs trace:write. Keep each
   * receipt's id: deleteTrace(id) erases the trip. [] when nothing was recorded or under 2 points. A failed send
   * rejects and keeps the unsent points in memory: call stopTrace() again to retry, cancelTrace() to drop them.
   */
  stopTrace(options?: CallOptions): Promise<TraceReceipt[]>;
  /** Stops recording and drops the trip unsent: the user changed their mind. */
  cancelTrace(): void;
  /**
   * Starts sending this vehicle's position for live traffic (POST /v1/traffic/probes), every 30 seconds while fixes
   * come. For fleets whose contract with dock covers their vehicles' positions: the API answers feature_off to any
   * other organization, and probes stop. Needs trace:write. No driver, phone, plate or device is sent: the vehicle is
   * a random token made anew every 15 minutes. Points stay in memory, never in storage. Throws when probes are
   * already on or there is no geolocation.
   */
  startProbes(options?: ProbeOptions): void;
  /** true while probes are on. */
  get probing(): boolean;
  /** Stops probes and sends what is left. null when nothing was left to send. */
  stopProbes(options?: CallOptions): Promise<ProbeReceipt | null>;
  /**
   * Live speed per road (GET /v1/traffic): the last 15 minutes that have ended, only roads that 5 or more fleet
   * vehicles passed. km_h is their moving meters over moving seconds, to 0.1. No road means too few vehicles, not an
   * empty road. bbox inside Mongolia, sides at most 1 degree. Needs insights:read.
   */
  traffic(bbox: BBox, options?: CallOptions): Promise<Traffic>;
  /**
   * The usual speed per road for this hour band (GET /v1/traffic/typical), for a map layer. Never live: show it as
   * typical.label ("ердийн"). Needs tiles:read only, the scope of a map key; counted like a tile request. No road
   * means no speed: features can be empty. bbox inside Mongolia, sides at most 1 degree.
   */
  trafficTypical(bbox: BBox, options?: CallOptions): Promise<TypicalTraffic>;
  /**
   * Opens an event for its live page and heat map (POST /v1/events): a name, an area and a time. For organizers whose
   * contract with dock is signed; any other organization gets feature_off. Needs trace:write. Every point is erased 15
   * minutes after the end; the heat cells stay.
   */
  createEvent(input: EventInput, options?: CallOptions): Promise<DockEvent>;
  /** One of your organization's events (GET /v1/events/{id}). Needs trace:read. */
  event(id: string, options?: CallOptions): Promise<DockEvent>;
  /**
   * Starts sending this participant's position to an event (POST /v1/events/{id}/positions), every 10 seconds while
   * fixes come, only after the participant said yes. No name, bib, phone or device is sent: the participant is a random
   * token made at start. Points stay in memory, never in storage. Sending stops on event_closed, not_found or
   * feature_off. Needs trace:write. Throws when already sending or there is no geolocation.
   */
  startEventPositions(eventId: string, options: EventPositionOptions): void;
  /** true while positions are being sent to an event. */
  get sendingPositions(): boolean;
  /** Stops sending positions and sends what is left. null when nothing was left to send. */
  stopEventPositions(options?: CallOptions): Promise<EventPositionsReceipt | null>;
  /**
   * Live dots for your event's page (GET /v1/events/{id}/live): each participant's last point of the last 2 minutes, to
   * 4 decimals, sorted by place, nothing about the participant; empty when fewer than 5 are live. Needs trace:read.
   */
  eventLive(id: string, options?: CallOptions): Promise<EventLive>;
  /**
   * Heat per area for your event (GET /v1/events/{id}/heat): participants per 100 m cell, cells with 5 or more, for the
   * whole event or one 15-minute window (an ISO time or Date on the quarter hour). Needs trace:read.
   */
  eventHeat(id: string, options?: CallOptions & {
    window?: string | Date;
  }): Promise<EventHeat>;
  /** One of your organization's trips as stored, GeoJSON [lon, lat, t]: the export. Needs trace:write. */
  trace(id: string, options?: CallOptions): Promise<Trace>;
  /** Erases a trip's points and times now. The id is kept, never reused. Twice is fine. Needs trace:write. */
  deleteTrace(id: string, options?: CallOptions): Promise<TraceDeleted>;
  /**
   * Trips per 500 m cell in a month, all partners together (PLAN.md 18, 19). `month` is YYYY-MM (Ulaanbaatar),
   * published 14 days after it ends (400 month_open before). `bbox` inside Mongolia, sides at most 1 degree. A cell
   * shows only with trips from 3 or more organizations; no cell means too few, not zero. Needs insights:read.
   */
  traceCells(month: string, bbox: BBox, options?: CallOptions): Promise<TraceCells>;
  /**
   * Moving speed per 500 m cell in a month, all partners together: where traffic is slow (PLAN.md 18, 19). The same
   * grid, month and bbox as traceCells. km_h is the counted meters over the counted seconds, to 0.1; parking and the
   * last 200 m are left out. Per cell, not per road. A cell shows only under the trip cells rule; no cell means too
   * few trips, not a slow road. No trip count. Needs insights:read.
   */
  traceSpeeds(month: string, bbox: BBox, options?: CallOptions): Promise<TraceSpeeds>;
  /**
   * Moving speed per road in a month, all partners together: speed profiles per road (PLAN.md 18, 19). Each trip is
   * matched to OSM ways with Valhalla when it is stored; a segment counts on a way under the traceSpeeds rule when both
   * its points matched that way. km_h is the counted meters over the counted seconds, to 0.1. A road shows only under
   * the trip cells rule; no road means too few trips, not a slow road. The line is dock's current road with that OSM id.
   * No trip count. `band` gives one hour band's trips only, under the same rule on its own trips; left out, all hours.
   * `rule.hours` says which. band needs API 0.49.0. Needs insights:read.
   */
  traceRoads(month: string, bbox: BBox, options?: CallOptions & {
    band?: RoadBand;
  }): Promise<TraceRoads>;
  /**
   * Your organization's own trips per cell, the same grid: your dashboard (PLAN.md 18, 20.13). Every cell with one of
   * your trips, no other organization read. The month you are in is answered too; `closed` is true once the counts are
   * final. Needs trace:read, a read-only scope: trace:write does not read it.
   */
  ownTraceCells(month: string, bbox: BBox, options?: CallOptions): Promise<OwnTraceCells>;
  /**
   * Your organization's arrival minutes in a month: how long your trips spend in the last 200 m, parking and the walk
   * to the door included (PLAN.md 18). `month` is YYYY-MM (Ulaanbaatar). Month totals with median, p75 and p90, trips
   * per minute, and up to 100 end cells on the trip cells grid, slowest first. No other organization is read.
   * `closed` is true from 14 days after the month ends. Needs trace:read.
   */
  ownArrivals(month: string, options?: CallOptions): Promise<OwnArrivals>;
  /**
   * When your organization's trips start in a month: each trip once, at the Ulaanbaatar weekday and hour of its first
   * stored point (PLAN.md 18). `month` is YYYY-MM (Ulaanbaatar). Month total, 7 weekday counts, 24 hour counts, and
   * every weekday and hour with trips. No other organization is read. `closed` is true from 14 days after the month
   * ends. Needs trace:read.
   */
  ownHours(month: string, options?: CallOptions): Promise<OwnHours>;
  /**
   * When trips start in a month, all partners together: road load by weekday and hour (PLAN.md 18, 19). `month` is
   * YYYY-MM (Ulaanbaatar), published 14 days after it ends (400 month_open before). A weekday and hour shows only under
   * the trip cells rule; no slot means too few, not zero. No total. Needs insights:read.
   */
  allHours(month: string, options?: CallOptions): Promise<AllHours>;
  /**
   * Where and when trips end in a month, all partners together: trips per khoroo and Ulaanbaatar hour of arrival
   * (PLAN.md 18, 19). `month` is YYYY-MM (Ulaanbaatar), published 14 days after it ends (400 month_open before). A
   * khoroo and hour shows only under the trip cells rule; no hour means too few, not zero. No khoroo total. Current
   * khoroo polygons only. Needs insights:read.
   */
  traceKhoroos(month: string, options?: CallOptions): Promise<TraceKhoroos>;
  /**
   * Your organization's own trips per khoroo and Ulaanbaatar hour of arrival in a month: your dashboard (PLAN.md 18,
   * 20.13). `month` is YYYY-MM (Ulaanbaatar), an open month too; `closed` once final. No group rule. Each trip counts
   * once: in one khoroo, or in `no_khoroo` where no current khoroo polygon holds its end. Needs trace:read.
   */
  ownKhoroos(month: string, options?: CallOptions): Promise<OwnKhoroos>;
  /**
   * Your organization's own trips per kind of building at the trip end in a month: what kind of deliveries you make
   * (PLAN.md 18, 20.13). `month` is YYYY-MM (Ulaanbaatar), an open month too; `closed` once final. No group rule. Each
   * trip counts once, by the building type when it was stored; `unknown` is a building with no type, `none` no building
   * within 30 m. Needs trace:read.
   */
  ownKinds(month: string, options?: CallOptions): Promise<OwnKinds>;
  /**
   * Point to the places at and around it, nearest first. Needs geocode:read. `types` (API 0.69.0, Mapbox's types): only
   * these kinds, chosen before the limit, so limit doors are the nearest doors; sent once each, in the order given.
   * Absent or empty: every kind. Another kind is refused here with the API's 400 invalid_types: no request. `point` is [lon, lat], longitude first: a Leaflet
   * latlng goes in as [lng, lat]. Sent as lon; the API reads lng as lon too, for a hand-written request.
   */
  reverse(point: LonLat, options?: CallOptions & {
    radius?: number;
    limit?: number;
    types?: ReverseType[];
  }): Promise<ReverseResponse>;
  /**
   * Places of these categories near a point, nearest first, each with its category and distance (API 0.80.0). Needs
   * geocode:read. A chip such as "Эмийн сан" is ['pharmacy'], "Хоол" ['restaurant', 'cafe']. radius in metres, 1000
   * when absent, at most 10000; limit 10 when absent, at most 20. Categories are sent once each, in the order given;
   * none, or another category, is refused here with the API's 400 invalid_category: no request. `point` is [lon, lat], longitude first: a Leaflet
   * latlng goes in as [lng, lat]. Sent as lon; the API reads lng as lon too, for a hand-written request.
   * photo: true adds each landmark's or building's photo (or null) to its row, in the same read.
   */
  nearby(point: LonLat, categories: NearbyCategory[], options?: CallOptions & {
    radius?: number;
    limit?: number;
    photo?: boolean;
  }): Promise<NearbyResponse>;
  /**
   * A door from its 5-character code. Cyrillic lookalike keys are accepted. `unit` is the flat or office
   * typed after the code ("7QSVX 17"). Needs door:read. 404 not_found, 410 retired_code, 400 invalid_code.
   * A text that cannot be a code (parseDoorCode) is refused here with the API's 400 invalid_code: no request.
   */
  door(code: string, options?: CallOptions & {
    unit?: string;
  }): Promise<Door>;
  /**
   * The address unit at a point, for a tap on the map: the хашаа that holds it, else the smallest building (GET
   * /v1/door/at). Needs door:read. `code` is null while the unit has none. 404 not_found when no unit holds the point,
   * and always inside a restricted site. `point` is [lon, lat], longitude first: a Leaflet latlng goes in as
   * [lng, lat]. Sent as lon; the API reads lng as lon too, for a hand-written request.
   */
  doorAt(point: LonLat, options?: CallOptions): Promise<UnitAt>;
  /**
   * Register a spoken name for one of your doors ("Сансар бэйкери" for 7QSVX), held for a year (API 0.52.0). Your own
   * name with another code moves to that door. Needs names:write and the door_names feature (403 feature_off until
   * dock opens it for your organization). 409 name_taken, 410 retired_code, 404 not_found, 422 too_many (50 names).
   */
  registerDoorName(name: string, code: string, options?: CallOptions): Promise<DoorNameReceipt>;
  /** Your organization's door names, A to Z, expired ones flagged until another organization takes them. Needs names:write. */
  doorNames(options?: CallOptions): Promise<OwnDoorNames>;
  /**
   * The door a name names, as door() answers it, official address first. Any spelling of the name finds it: case,
   * spaces, punctuation and Latin lookalike letters. Needs door:read and the door_names feature. 404 not_found counts
   * against the door miss cap. A name the API would refuse (no letter, a phone number or an e-mail) is refused here
   * with its 400: no request, so it never travels in a URL.
   */
  doorByName(name: string, options?: CallOptions): Promise<NamedDoor>;
  /**
   * A one-time code that lets another organization claim one of your names (API 0.53.0). Shown once; a new one
   * replaces the old. Works for 7 days, or until the name expires if sooner. Needs names:write.
   */
  doorNameTransferCode(name: string, options?: CallOptions): Promise<DoorNameTransfer>;
  /** Clears the waiting transfer code of one of your names. `cancelled` is false when none was waiting. Needs names:write. */
  cancelDoorNameTransfer(name: string, options?: CallOptions): Promise<DoorNameTransferCancel>;
  /**
   * Take a name with the transfer code its holder gave you. The door and expires_at stay; the code is used up. Needs
   * names:write. 409 transfer_refused (wrong, cleared or timed out; counts a door miss), 409 already_yours, 422 too_many.
   */
  claimDoorName(name: string, transferCode: string, options?: CallOptions): Promise<DoorNameClaim>;
  /**
   * Write how guests find the entrance of the door one of your names points at, in mn, en or both (API 0.58.0). The
   * note waits for a person to review it; door() serves the notes the map holds until then. Writing again replaces
   * your waiting text. Needs names:write and the hotel.notes feature (403 feature_off until dock opens it). 400
   * personal_data for a phone number or e-mail in a note, invalid_notes, invalid_body; 404 not_found for a name your
   * organization does not hold. A name the API would refuse is refused here with its 400: no request.
   */
  writeDoorNotes(name: string, notes: DoorNotesBody, options?: CallOptions): Promise<DoorNoteReceipt>;
  /**
   * Your notes for the door one of your names points at, each waiting, accepted or refused (API 0.59.0). Never another
   * organization's. Needs names:write and the hotel.notes feature; 404 not_found as for writeDoorNotes.
   */
  doorNotes(name: string, options?: CallOptions): Promise<DoorNotes>;
  /**
   * Send a gate photo for the door one of your names points at: the entrance as a guest sees it (API 0.60.0). One
   * JPEG as the camera saved it, up to 2 MB, 320 to 8000 px a side. The API removes its metadata (GPS, the phone,
   * the time) before it stores it. A person reviews it and refuses one with a face, a car plate or a phone number.
   * Sending again replaces your waiting photo. Needs names:write and the hotel.notes feature (403 feature_off). 400
   * not_jpeg, invalid_photo, photo_too_small, photo_too_big; 413 too_large; 404 not_found as for writeDoorNotes.
   */
  sendDoorPhoto(name: string, jpeg: Uint8Array | ArrayBuffer | Blob, options?: CallOptions): Promise<DoorPhotoReceipt>;
  /**
   * The gate photo a person accepted last for the door one of your names points at, for your app to show your guests
   * (API 0.61.0). Never a waiting or refused photo, never another organization's. Show it inside your app only, never
   * on a public page. Pass the sha256 you hold: when it is still the photo, jpeg is null and no bytes travel. Needs
   * names:write and the hotel.notes feature. 404 not_found also when no photo of yours was accepted.
   */
  doorPhoto(name: string, options?: CallOptions & {
    sha256?: string;
  }): Promise<DoorPhoto>;
  /**
   * Route between two ends, each a building code or [lon, lat]. A code routes to the street side of its building or the gate of its yard; `to.gap_m` is the
   * walk left from the road. Needs route:read, and door:read for a code. 422 no_route, end_in_closure (an end
   * inside a closure active today; closureAt checks offline), closures_over_limit, 404 not_found. departAt (API 0.50.0):
   * a driving route's measured reads the speeds of its hour band; a Date is sent in UTC, a string as written (ISO 8601
   * with an offset). 400 invalid_depart_at. via (API 0.67.0): up to ROUTE_VIA_MAX ends passed on the way, in order,
   * each a code or [lon, lat]; the answer has one leg per hop and each via end where the line passes it. An empty list
   * is none. A long list, or a code holding ";", is refused here with the API's 400 invalid_via: no request. 404
   * not_found names a via end by its place ("via[1]: ...").
   */
  route(from: string | LonLat, to: string | LonLat, options?: CallOptions & {
    mode?: RouteMode;
    departAt?: Date | string;
    via?: (string | LonLat)[];
  }): Promise<Route>;
  /**
   * Free-flow time and distance from every end in `from` to every end in `to`, in one call (API 0.63.0): up to
   * MATRIX_MAX by MATRIX_MAX, each end a building code or [lon, lat] as in route. `durations_s[i][j]` goes from `from[i]` to
   * `to[j]`; null where no path joins them. Needs route:read, and door:read for a code. An empty or long list, or a
   * code holding ";", is refused here with the API's 400 invalid_from or invalid_to: no request. 404 not_found names
   * the end by its place ("to[1]: ..."); 422 no_route, end_in_closure, closures_over_limit as for route.
   */
  matrix(from: (string | LonLat)[], to: (string | LonLat)[], options?: CallOptions & {
    mode?: RouteMode;
  }): Promise<Matrix>;
  /**
   * The area reached from one end within each of `contoursMinutes` (API 0.64.0), as a GeoJSON FeatureCollection: one
   * Polygon or MultiPolygon per contour, the largest first. A map takes it as a source as it is. `from` is a building code
   * (from its street side or gate) or [lon, lat] as in route. 1 to ISOCHRONE_CONTOURS_MAX whole minutes from 1 to
   * ISOCHRONE_MINUTES_MAX, increasing; anything else is refused here with the API's 400 invalid_contours: no request.
   * Needs route:read, and door:read for a code. 404 not_found; 422 no_route (no road near from), end_in_closure,
   * closures_over_limit, not_accepted as for route. Free-flow times: no departAt.
   */
  isochrone(from: string | LonLat, contoursMinutes: number[], options?: CallOptions & {
    mode?: RouteMode;
  }): Promise<Isochrone>;
  /**
   * A GPS trace matched to the road network (API 0.65.0): the roads it went along, one matching per unbroken piece, and
   * one tracepoint per point, null where no road was found or the point is in a restricted site. `coordinates` is 2 to
   * MATCHING_MAX [lon, lat] in the order recorded; `timestamps`, optional, one whole Unix second per point, never going
   * back. Anything else is refused here with the API's 400 invalid_coordinates or invalid_timestamps: no request. Needs
   * route:read. 422 no_match (no road near the trace), not_accepted (fewer than 2 points outside restricted sites).
   * Closures are not avoided. The API neither stores nor logs the trace; to share a trip, use startTrace.
   */
  matching(coordinates: LonLat[], options?: CallOptions & {
    timestamps?: number[];
    mode?: RouteMode;
  }): Promise<Match>;
  /**
   * The order to visit stops in with the least time on the road, and the trip in that order (API 0.66.0). `stops` is 2
   * to OPTIMIZED_TRIPS_MAX ends, each a building code (to its street side or gate) or [lon, lat] as in route; the trip starts at the
   * first. `roundtrip` true, the API's default, goes back to it; false ends at the last sent. `waypoints` are the stops
   * in the order sent, each with `waypoint_index`, its place in the trip. A short or long list, or a code holding ";", is
   * refused here with the API's 400 invalid_coordinates: no request. Needs route:read, and door:read for a code. 404
   * not_found names the stop by its place ("coordinates[1]: ..."); 422 no_route (a stop no road reaches), not_accepted,
   * end_in_closure, closures_over_limit as for route. Free-flow times, no time at the stops.
   */
  optimizedTrips(stops: (string | LonLat)[], options?: CallOptions & {
    roundtrip?: boolean;
    mode?: RouteMode;
  }): Promise<OptimizedTrip>;
  /**
   * The buildings and entrances at or near a point (API 0.68.0), as Mapbox's Tilequery API: what a tap on the map is
   * on. `sources` is building, entrance or both (the default), each once. `radius` in metres, 0 to
   * TILEQUERY_RADIUS_MAX_M, sent to the centimetre; 0, the API's default, finds the building the point is inside, and an
   * entrance needs a radius. `limit` 1 to TILEQUERY_LIMIT_MAX, the API's default 5. `geometry` keeps one type. GeoJSON,
   * nearest first: each feature the Point of it nearest the point sent, its tile fields and `tilequery` (distance in
   * metres, geometry, layer). No building codes: `door` takes a code. Anything the API would refuse is refused here with its
   * error: 404 not_found (a source), 400 invalid_point, invalid_radius, invalid_limit, invalid_geometry; no request.
   * Needs tiles:read; counts against rate_limit and daily_quota. Coverage: Ulaanbaatar.
   */
  tilequery(point: LonLat, options?: CallOptions & {
    sources?: TilequerySource[];
    radius?: number;
    limit?: number;
    geometry?: TilequeryGeometry;
  }): Promise<Tilequery>;
  /**
   * Road closures not over today (PLAN.md 20.14), each with the ring Valhalla avoids. No partner name, no note. Keep
   * the answer for offline use; excludePolygons(closures, day) picks the rings for a route day. ids_sha256 changes
   * when a closure is added or lifted. Needs route:read.
   */
  closures(options?: CallOptions): Promise<ClosuresResponse>;
  /**
   * Report a stretch or a crossing that cannot be driven (PLAN.md 20.14). Kept forever: never edited or deleted, lift
   * it when the road opens. Needs closures:write. Pass `idempotencyKey` (a uuid fits), and the same key and report when
   * you retry after a timeout: the API answers the first report and stores nothing twice; the same key with another
   * report is 422 idempotency_key_reused. A key the API would refuse is refused here with its 400 bad_idempotency_key:
   * no request. 400 note_has_phone, outside_mongolia, line_too_long, restricted and the rest come from the API.
   */
  reportClosure(report: ClosureReport, options?: CallOptions & {
    idempotencyKey?: string;
  }): Promise<Closure>;
  /** One of your organization's closure reports as stored, note included: your export. Needs closures:write. 404 not_found for others' ids. */
  closure(id: string, options?: CallOptions): Promise<Closure>;
  /**
   * The road is open again: lift one of your reports from `on` (YYYY-MM-DD Ulaanbaatar), today by default, never a
   * later day (400 lift_in_future) nor before its from (400 lift_before_from). Lifting twice keeps the first day.
   * Needs closures:write.
   */
  liftClosure(id: string, options?: CallOptions & {
    on?: string;
  }): Promise<Closure>;
  /** Tell dock a door is wrong. A person reviews it; door data does not change until then. Needs door:read. */
  reportDoor(report: DoorReport, options?: CallOptions): Promise<DoorReportReceipt>;
  /**
   * One place again, by the id geocode, reverse or changes gave: the place fields plus `parents`. Needs geocode:read.
   * 410 retired (the id is never reused), 404 not_found (unknown, restricted, or added since the last index refresh).
   */
  place(id: string, options?: CallOptions): Promise<Address>;
  /**
   * A building's entrances in one call, for its card: each with its code once a door is minted and its level
   * (matched, partial, none), numbered first (API 0.84.0). The id is a building's, from geocode, reverse or the
   * building tile. Needs geocode:read. 410 retired, 404 not_found (unknown, not a building, or restricted).
   */
  entrances(buildingId: string, options?: CallOptions): Promise<BuildingEntrances>;
  /**
   * The organizations registered in a building, by floor, for "Энд байгаа байгууллагууд" on an office building or
   * mall card. The id is a building's, from geocode, reverse or the building tile, or its unit code (a хашаа code
   * answers an empty list). Only organizations matched to an
   * official registration are listed; a floor or unit the register does not give is null. Needs geocode:read.
   * 410 retired, 404 not_found (unknown, not a building, or restricted).
   */
  directory(buildingId: string, options?: CallOptions): Promise<BuildingDirectory>;
  /**
   * The building entrances nearest a point, nearest first, each as entrances() lists it plus its building and
   * distance_m (API 0.85.0): for a long-press on the map or "Энд байгаа хаалга" from the user's position. Generated
   * entrances count (source generated, confidence 0: show them as таамаг). radius in metres, 50 when absent, at most
   * 1000; limit 1 when absent, at most 20. Needs geocode:read. `point` is [lon, lat], longitude first: a Leaflet
   * latlng goes in as [lng, lat]. Sent as lon; the API reads lng as lon too, for a hand-written request.
   */
  entrancesNear(point: LonLat, options?: CallOptions & {
    radius?: number;
    limit?: number;
  }): Promise<NearEntrances>;
  /**
   * Indoor maps at a map position (API 0.105.0): venues whose outline holds the point or lies within 200 m, nearest
   * first, at most 10, each with its id, name, category, distance and floors. latlng goes in as [lng, lat]. Needs door:read.
   */
  venuesNear(point: LonLat, options?: CallOptions): Promise<NearVenues>;
  /**
   * A building with an indoor map (API 0.100.0): its name, outline and floors, lowest first. The id is the
   * `indoor.venue_id` a geocode row carries. Needs door:read. 404 not_found for an unknown id.
   */
  venue(venueId: string, options?: CallOptions): Promise<Venue>;
  /**
   * One floor of a venue as GeoJSON: its units, openings and amenities, each with source and confidence. `level` is
   * the floor number (0 ground, -1 basement). Needs door:read. 404 not_found for an unknown venue or floor.
   */
  venueLevel(venueId: string, level: number, options?: CallOptions): Promise<VenueLevel>;
  /**
   * What a printed plate for one unit says: venue, floor, the unit's name or number, a point, and the `link` its QR
   * holds. `unitId` is a unit's id from venueLevel. Needs door:read. 404 not_found for an unknown venue or unit.
   */
  venueSpot(venueId: string, unitId: string, options?: CallOptions): Promise<VenueSpot>;
  /**
   * The walk inside a venue to one unit, floor by floor, with steps in Mongolian and the floor on each. `from` is
   * [lon, lat] on floor `fromLevel` (0 when absent): the entrance a road route used, or a tapped spot. `unitId` is a
   * unit's id from venueLevel. Stairs, escalators and lifts join the floors. Needs door:read. 404 not_found for an
   * unknown venue or unit; 422 no_route where no drawn corridor joins the start and the unit (nothing is invented).
   */
  venueRoute(venueId: string, from: LonLat, unitId: string, options?: CallOptions & {
    fromLevel?: number;
  }): Promise<VenueRoute>;
  /**
   * The whole way to a unit in one answer: the road to the right street entrance, then the walk inside. `from` is
   * [lon, lat]; `mode` is driving when absent. Distance and duration are road plus walk. Needs route:read. 404
   * not_found; 422 no_entrance when no street entrance is drawn, 422 no_route when no road or no corridor reaches it.
   */
  venueDirections(venueId: string, from: LonLat, unitId: string, options?: CallOptions & {
    mode?: RouteMode;
  }): Promise<VenueDirections>;
  /**
   * Ids of places changed in [since, until), oldest first, one page. Needs changes:read (partner keys). Pass the
   * page's `next` as `cursor` until it is null, then poll again with since = until. First sync: since
   * '1970-01-01T00:00:00Z'. A string needs a zone (Z or +08:00); a Date is sent as UTC. limit 1 to 1000, default 100,
   * on every call: the cursor does not keep it.
   */
  changes(since: string | Date | null, options?: CallOptions & {
    limit?: number;
    cursor?: string | null;
  }): Promise<ChangesResponse>;
  /**
   * A driver on site says a door is there (confirmed), elsewhere (wrong_location) or gone (missing). Needs
   * confirm:write (partner keys). Map data does not change at once. 422 implausible when device is over 40 m from
   * the tap, too_far when the tap is over 500 m from the entrance: ask the driver to tap again on site.
   */
  confirm(confirmation: Confirmation, options?: CallOptions): Promise<ConfirmReceipt>;
  /**
   * The photo of the door the driver took with a confirmation, sent after confirm() answered, with its id (API
   * 0.71.0). One JPEG as the camera saved it, up to 2 MB, 320 to 8000 px a side; the API removes its metadata. One
   * photo per confirmation: the same photo again is unchanged, another is 409 photo_exists. 404 not_found when the id
   * is not one of your confirmations. Needs confirm:write.
   */
  sendConfirmPhoto(id: string, jpeg: Uint8Array | ArrayBuffer | Blob, options?: CallOptions): Promise<ConfirmPhotoReceipt>;
  /**
   * Your drivers' confirmations and door reports in a month: your dashboard (PLAN.md 18). `month` is YYYY-MM
   * (Ulaanbaatar). Counts by result for the month and each day on site, different drivers (never the ids), and up to
   * 100 failure points. The month you are in is answered too; `closed` is true from 8 days after it ends. No other
   * organization is read. Needs confirm:read, a read-only scope: confirm:write does not read it.
   */
  ownConfirmations(month: string, options?: CallOptions): Promise<OwnConfirmations>;
  /** This key's organization: caps and the last `days` days (1 to 90). Free, answers after the quota. */
  usage(options?: CallOptions & {
    days?: number;
  }): Promise<UsageResponse>;
  /** TileJSON URL of a tile source (building, entrance), without the key. */
  tileJsonUrl(source: string): string;
  /**
   * MapLibre transformRequest: adds the key to requests for this API only. Other hosts (a partner basemap,
   * fonts) get nothing.
   */
  readonly transformRequest: (url: string) => RequestParameters;
  /** The key never shows up in console.log, JSON.stringify or error reports. */
  toJSON(): {
    baseUrl: string;
  };
}

code.ts ​

ts
/**
 * Building codes on the device, the API's rules (api/src/modules/door/door-code.ts, PLAN.md 4c). Five characters,
 * digits and consonants, no vowels and no I, L, O, U. sdk/testdata/door-code.json holds the cases the API and
 * the three SDKs agree on.
 */
export const DOOR_CODE_ALPHABET = "0123456789BCDFGHJKMNPQRSTVWXYZ";

/** Characters in a building code. */
export const DOOR_CODE_LENGTH = 5;

/**
 * What parseDoorCode read. `unit` is whatever follows the code (flat number, household name), as typed.
 * `error` says why not: empty, length (not 5 characters) or letters (a character no code uses). `reason` is the
 * API's invalid_code message, in English; show the person your own words for `error`.
 */
export type DoorCodeInput = {
  ok: true;
  code: string;
  unit: string | null;
} | {
  ok: false;
  error: 'empty' | 'length' | 'letters';
  reason: string;
};

/**
 * Reads a typed or pasted building code as the API would: "7QSVX", "7qsvx 17", "7QSVХ Бат". No request. Use it to
 * check a field as the person types, and send only `ok` codes to door(): a code that finds no door counts
 * against the daily miss cap; one that cannot be a code is caught here first.
 */
export function parseDoorCode(text: string): DoorCodeInput;

/**
 * A door link (PLAN.md 4a: "send the code, a link, or a QR"; PLAN 18 entry paths 1 and 4): this base, the code, and
 * `?u=` with the unit when there is one. sdk/testdata/door-link.json holds the cases the three SDKs agree on.
 */
export const DOOR_LINK_BASE = "https://map.dock.mn/d/";

/**
 * The link to a door, for a share sheet, a message or a QR: "https://map.dock.mn/d/7QSVX?u=17". `text` and `unit`
 * are read as parseDoorCode reads `text unit`, so the code may be typed in Cyrillic lookalikes and the unit (flat
 * number, household name) goes in percent-encoded. null when the text cannot be a code. No request: the link says
 * nothing about whether a door has this code.
 */
export function doorLink(text: string, unit?: string | null): string | null;

/**
 * Reads the first door link in a text: a pasted link, a share message with a link in it, or the URL a deep link
 * opened the app with. http or https or none, the host in any case, `?u=` for the unit, other parameters and `#`
 * ignored. null when the text has no door link; otherwise what parseDoorCode reads from the code and the unit,
 * so a link with a broken code is `{ ok: false }` with the same reasons. No request.
 */
export function parseDoorLink(text: string): DoorCodeInput | null;

qr.ts ​

ts
/**
 * A QR as rows of modules: `modules[y][x]` is true for a dark module. `size` modules a side (21 to 177), no quiet
 * zone: leave 4 light modules around it when drawing. `link` is the text it holds.
 */
export interface DoorQr {
  link: string;
  version: number;
  size: number;
  modules: boolean[][];
}

/** doorQrSvg drawing: `px` per module (default 4), `margin` light modules around the code (default 4, the spec's
 * minimum quiet zone). */
export interface DoorQrSvgOptions {
  px?: number;
  margin?: number;
}

/**
 * The QR of a door: the modules of doorLink(text, unit). null when the text cannot be a code. A link longer than
 * level M holds (2,331 bytes, a unit of about 2,300 characters) is also null.
 */
export function doorQr(text: string, unit?: string | null): DoorQr | null;

/**
 * The QR of a door as an SVG document, black on white, for an <img>, a share sheet or print. null as doorQr. Each
 * module is `px` wide; the SVG is (size + 2 * margin) * px a side.
 */
export function doorQrSvg(text: string, unit?: string | null, options?: DoorQrSvgOptions): string | null;

basemap.ts ​

ts
/** The base map's languages in the pack: names as name:mn and name:en. */
export type BasemapLang = 'mn' | 'en';

/** An offline pack's manifest block `basemap` (null in packs built before 2026-10-03, or with no base map). */
export interface PackBasemap {
  method: string;
  file: string;
  format: string;
  schema: string;
  schema_version: string;
  tile_type: string;
  tiles: number;
  minzoom: number;
  maxzoom: number;
  bounds: [number, number, number, number];
  layers: string[];
  excluded_layers: string[];
  languages: string[];
  osm_time: string | null;
  /** Show on the map: the source carries it, so MapLibre's attribution control shows it. */
  attribution: string;
  rule: string;
  rule_mn: string;
}

/** What basemapStyle() takes. */
export interface BasemapStyleOptions {
  /** Label language, one of the block's languages. Default 'mn'. A name missing in it falls back to OSM's name. */
  lang?: BasemapLang;
  /** A glyphs URL ({fontstack}, {range}). The pack carries no fonts: without glyphs the style has no labels. */
  glyphs?: string;
  /** Font stack on that glyphs URL. Default ['Noto Sans Regular']. */
  fonts?: string[];
  /** Background color. Default the dock style's paper tone. */
  background?: string;
}

/** Layers the base map must not carry: business names (shown only when registered) and OSM house numbers. */
export const BASEMAP_EXCLUDED: readonly ["poi", "housenumber"];

/**
 * A whole style for an offline pack's base map: land, water, tracks and roads, boundaries, buildings, and with
 * `glyphs` also place, water and road names. `url` is where the app keeps basemap.pmtiles (a file:// or http URL);
 * the source asks for it as pmtiles://. MapLibre Native reads pmtiles:// itself; MapLibre GL JS needs the pmtiles
 * protocol added. Throws TypeError for no block, RangeError for a block that is not the pack's base map or carries
 * a poi or housenumber layer, a lang the block lacks, or an empty url.
 */
export function basemapStyle(basemap: PackBasemap | null | undefined, url: string, options?: BasemapStyleOptions): Json;

closures.ts ​

ts
/** What to send Valhalla for one route day: the rings of the closures active that day. */
export interface ExcludePolygons {
  /** YYYY-MM-DD, Ulaanbaatar. */
  day: string;
  /** The closures avoided, in list order. Show them with the route. */
  ids: string[];
  /** Valhalla's exclude_polygons. */
  rings: LonLat[][];
  perimeter_m: number;
  vertices: number;
}

/**
 * The closures active on the route day are over Valhalla's limits for one request. Refuse the route: never drop a
 * closure to fit.
 */
export class ClosuresOverLimitError extends Error {
  /** The route day, YYYY-MM-DD Ulaanbaatar. */
  readonly day: string;
  /** The day's closure rings, metres of perimeter in all. */
  readonly perimeter_m: number;
  /** The day's closure rings, vertices in all. */
  readonly vertices: number;
  constructor(day: string, count: number, perimeterM: number, vertices: number, limits: ClosuresResponse['valhalla_limits']);
}

/** Today in Ulaanbaatar (UTC+8, no daylight saving), YYYY-MM-DD. */
export function ubToday(now?: number): string;

/** from <= day, and day <= to when to is set (the list's active_when). Days compare as YYYY-MM-DD strings. */
export function isActive(item: Pick<ClosureItem, 'from' | 'to'>, day: string): boolean;

/**
 * Offline: the rings to send Valhalla as exclude_polygons for a route on `day` (default today, Ulaanbaatar), from a
 * GET /v1/closures answer or an offline pack's closures block. Throws RangeError for a bad day or a day before the
 * list's as_of (closures that ended before it are not in it: fetch again), and ClosuresOverLimitError over Valhalla's
 * limits.
 */
export function excludePolygons(closures: ClosuresResponse, day?: string): ExcludePolygons;

/**
 * Offline: the id of the first closure in `avoid` whose ring holds `point`, or null. Valhalla drops every road inside
 * an exclude ring, so a route from or to that point has no road to start from: it answers "No path could be found
 * for input". Check both ends before asking your own Valhalla, as GET /v1/route does (422 end_in_closure).
 */
export function closureAt(avoid: Pick<ExcludePolygons, 'ids' | 'rings'>, point: LonLat): string | null;

crossings.ts ​

ts
/** Modelled monthly flow at a river crossing (lane D's dock_steppe.rivers). Rivers only: streams have none. */
export interface CrossingFlow {
  river_id: number;
  /** 1991 to 2020 monthly normals, January to December, m3/s. */
  normal_m3s: number[];
  /** Months 1 to 12 whose normal is at least 1.5 times the mean of the 12. */
  high_months: number[];
  /** agrees, doubtful (the model reach is 10x below its OSM river's), unchecked. */
  reach: 'agrees' | 'doubtful' | 'unchecked';
  /** 0.4, or LOW_FLOW_CONFIDENCE (0.1) on a doubtful reach and on snap none_fits or main_stem. */
  confidence: number;
  /** How the reach was picked. matched: it follows the OSM river. none_fits: none does, the nearest is kept.
   * main_stem: the channel follows only a small reach, the river's main stem off_m away is taken. nearest: the
   * nearest, no lines to match on. Absent in packs built before the match. */
  snap?: 'matched' | 'none_fits' | 'main_stem' | 'nearest';
}

/** A delta channel: the river splits into a lake there and no model reach says what share a channel carries. The
 * crossing has flow null. Rule text in the pack's flow.delta.rule and rule_mn. */
export interface CrossingDelta {
  /** The lake's OSM name, folded to one script when it mixed Latin and Cyrillic in a word. */
  lake: string | null;
  /** The lake's OSM name as written, when lake is folded. Absent in the pack when lake is the OSM text, and in packs
   * before national crossings v11; null on a warning then. Show lake. */
  lake_name_osm?: string | null;
  /** The lake's OSM element, as relation/114846. */
  lake_osm: string;
  /** The river whose mouths form the delta. */
  river: string;
}

/** An unnamed river or stream whose two ends lie on one named OSM river: a channel of it. From the OSM drawing, not
 * checked on the ground. Rule text in the pack's channel_rule and channel_rule_mn. */
export interface CrossingChannelOf {
  /** The river's OSM name, as on its river_way_id way. */
  name: string;
  name_en: string | null;
  /** The OSM way the name was read from. */
  river_way_id: number;
}

/** An unnamed river or stream, not a channel, and the named OSM river its water reaches downstream by the direction
 * the ways are drawn. From the OSM drawing, not checked on the ground. Rule text in the pack's tributary_rule and
 * tributary_rule_mn. */
export interface CrossingFlowsInto {
  /** The river's OSM name, as on its river_way_id way. */
  name: string;
  name_en: string | null;
  /** The OSM way the name was read from. */
  river_way_id: number;
  /** The fewest unnamed ways from this crossing's way to the river, its own included. */
  ways: number;
}

/** The OSM tags of the route ways at a crossing. Rule text in the pack's road_rule and road_rule_mn. */
export interface CrossingRoad {
  /** The ways' OSM highway tags, as track or trunk. */
  highway: string[];
  /** The ways' OSM surface tags; empty when none has one. */
  surface: string[];
  /** true: a way has a paved surface. false: every way has an unpaved one. null: a way has no surface tag. A none
   * crossing on a paved road may be a bridge or culvert OSM does not tag; not checked. */
  paved: boolean | null;
}

/** At or under this flow confidence the model reach is not sure to be this river's: show the months, not the m3/s. */
export const LOW_FLOW_CONFIDENCE = 0.1;

/** One crossing point in an offline pack's crossings.geojson (files[1]). */
export interface CrossingFeature {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: {
    /** ford: OSM says ford. none: OSM has no bridge or ford there, not that there is none. */
    kind: 'ford' | 'none';
    /** river, stream, canal, or unknown on a ford node with no waterway line (ford_node_id set). */
    waterway: string;
    name: string | null;
    /** The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. Absent
     * when name is the OSM text. Show name. Packs before offline v13 lack it. */
    name_osm?: string;
    name_en: string | null;
    intermittent: boolean;
    flow?: CrossingFlow | null;
    /** Set on a delta channel, with flow null. Absent in packs built before the delta rule. */
    delta?: CrossingDelta | null;
    /** Set on an unnamed channel of a named river. Absent in packs built before the channel rule. */
    channel_of?: CrossingChannelOf | null;
    /** Set on an unnamed river or stream that reaches one named river. Absent in packs built before the tributary
     * rule. */
    flows_into?: CrossingFlowsInto | null;
    /** Null on a new track alone (no OSM way). Absent in packs built before the road rule. */
    road?: CrossingRoad | null;
    /** The OSM ford node, on a ford whose water OSM does not draw as a line; waterway is unknown. Null on line
     * crossings, absent in packs built before the ford node rule. */
    ford_node_id?: number | null;
  };
}

/** An offline pack's crossings.geojson. Check its sha256 against the manifest's files[1] before use. */
export interface PackCrossings {
  type: 'FeatureCollection';
  /** A route passes a crossing when its line comes this close, m. */
  match_m: number;
  note: string;
  note_mn: string;
  /** Null in packs built before the flow block. */
  flow?: {
    attribution: string;
  } | null;
  /** Crossings with road.paved true. Absent in packs built before the road rule, as the two rule texts. */
  on_paved?: number;
  road_rule?: string;
  road_rule_mn?: string;
  /** None crossings that became the crossing of a bridge drawn just off the water, so left out of the pack. Absent in
   * packs built before the bridge rule, as the two rule texts. */
  near_bridge?: number;
  bridge_rule?: string;
  bridge_rule_mn?: string;
  /** Crossings joined into the one beside them because their water way is split into two ways at the road. Absent in
   * packs built before the split rule, as the two rule texts. */
  one_point?: number;
  split_rule?: string;
  split_rule_mn?: string;
  /** Crossings marked intermittent from OSM seasonal=* alone, no intermittent tag. Absent in packs built before that
   * rule, as the two rule texts. */
  from_seasonal?: number;
  intermittent_rule?: string;
  intermittent_rule_mn?: string;
  /** Fords from an OSM ford node alone, no waterway line drawn. Absent in packs built before that rule, as the two
   * rule texts. */
  ford_nodes?: number;
  ford_node_rule?: string;
  ford_node_rule_mn?: string;
  /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. Packs before offline v13 lack
   * them. */
  names_folded?: number;
  name_rule?: string;
  name_rule_mn?: string;
  features: CrossingFeature[];
}

/** A ford or unbridged crossing the route passes. */
export interface CrossingWarning {
  kind: 'ford' | 'none';
  point: LonLat;
  waterway: string;
  name: string | null;
  /** The OSM text when name was folded to one script, else null. Show name. */
  name_osm: string | null;
  name_en: string | null;
  intermittent: boolean;
  /** Along the route to its point nearest the crossing, m, rounded. A tie goes to the earlier pass. */
  along_m: number;
  /** From the route line, m, one decimal. */
  off_m: number;
  /** The month is one of high_months. Null when the crossing has no flow (streams, canals). */
  high_water: boolean | null;
  high_months: number[];
  /** The month's normal, m3/s. Null with no flow, on a doubtful reach, and at LOW_FLOW_CONFIDENCE or under (snap
   * none_fits, main_stem): show the months, not the number. */
  month_m3s: number | null;
  reach: CrossingFlow['reach'] | null;
  confidence: number | null;
  /** The flow's snap. Null with no flow or in a pack built before the match. */
  snap: CrossingFlow['snap'] | null;
  /** A delta channel: say "delta channel, flow unknown". Null elsewhere and in packs built before the delta rule. */
  delta: CrossingDelta | null;
  /** An unnamed channel of a named river: say "unnamed channel of <name>". Null elsewhere and in packs built before
   * the channel rule. */
  channel_of: CrossingChannelOf | null;
  /** An unnamed river or stream that reaches a named river: say "unnamed, flows into <name>". channel_of wins when
   * both are set. Null elsewhere and in packs built before the tributary rule. */
  flows_into: CrossingFlowsInto | null;
  /** The road the route crosses on. With kind none and paved true, say "paved road, OSM tags no bridge here". Null
   * on a new track alone and in packs built before the road rule. */
  road: CrossingRoad | null;
  /** The OSM ford node of a ford whose water OSM does not draw as a line, waterway unknown: say "OSM ford, water not
   * drawn". Null on line crossings and in packs built before the ford node rule. */
  ford_node_id: number | null;
}

/** What crossingsOnRoute() returns: the warnings in route order, with the pack's note. */
export interface RouteCrossings {
  /** 1 to 12. */
  month: number;
  match_m: number;
  /** In route order. */
  warnings: CrossingWarning[];
  /** Show with the warnings: from OSM tags, not field checked. */
  note: string;
  note_mn: string;
  /** Show when a warning has flow; else null. */
  flow_attribution: string | null;
  /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. Null when the pack has no rule. */
  name_rule: string | null;
  name_rule_mn: string | null;
}

/**
 * Offline: the ford and unbridged crossings a route line passes, from an offline pack's crossings.geojson, in route
 * order. `month` (1 to 12, default this month in Ulaanbaatar) sets high_water. Throws RangeError for a line under 2
 * points or a bad month.
 */
export function crossingsOnRoute(crossings: PackCrossings, line: LonLat[], month?: number): RouteCrossings;

pack.ts ​

ts
/** One row of an offline pack manifest's `files`: a file in the pack folder, its size and sha256. */
export interface PackFile {
  name: string;
  bytes: number;
  /** Lower case hex. */
  sha256: string;
}

/** The part of an offline pack's manifest.json checkPack reads; pass the whole parsed manifest. */
export interface PackManifestFiles {
  files: PackFile[];
}

/** ok: size and sha256 match. missing: no such file. bytes: another size, not hashed. sha256: same size, other bytes. */
export type PackFileStatus = 'ok' | 'missing' | 'bytes' | 'sha256';

/** One file of a pack manifest as checkPack() found it. */
export interface PackFileCheck {
  name: string;
  status: PackFileStatus;
  /** The file's size, null when missing. */
  bytes: number | null;
  expected_bytes: number;
  /** The file's sha256, null when missing or another size. */
  sha256: string | null;
  expected_sha256: string;
}

/** What checkPack() returns. */
export interface PackCheck {
  /** Every listed file is there with its size and sha256. Use no file from the pack otherwise. */
  ok: boolean;
  /** In manifest order. */
  files: PackFileCheck[];
  /** Names of the files not ok, in manifest order. Download them again. */
  bad: string[];
}

/** A file's bytes by its name in the pack folder; null or undefined when it is not there. */
export type PackFileReader = (name: string) => Promise<Uint8Array | ArrayBuffer | null | undefined>;

/**
 * Offline: checks each file the manifest lists, in its order, by size and then sha256 (Web Crypto). Files the manifest
 * does not list are not read. Throws RangeError for a manifest with no files, a name that is not a plain file name, a
 * name listed twice, a bytes value that is not a whole number from 0, or a sha256 that is not 64 lower case hex.
 * An error thrown by `read` comes through.
 */
export function checkPack(manifest: PackManifestFiles, read: PackFileReader): Promise<PackCheck>;

packroute.ts ​

ts
/** The part of an offline pack's manifest.json packRoute reads; pass the whole parsed manifest. */
export interface PackRouteManifest {
  id: string;
  /** [west, south, east, north]. */
  bbox: [number, number, number, number];
  /** What every route on the pack sends: costing auto, costing_options.auto.use_tracks 0.5 today. */
  request: {
    costing: string;
    costing_options?: Record<string, unknown>;
  };
  /** scored, or osm_only: no trace scores yet. */
  confidence: string;
  /** What confidence means, for the driver, en and mn. Packs from offline v10; absent before. */
  confidence_note?: string | null;
  confidence_note_mn?: string | null;
  attribution: string;
  note: string;
  closures: ClosuresResponse;
}

/** What packRoute() takes. */
export interface PackRouteOptions {
  /** Route day, YYYY-MM-DD Ulaanbaatar. Default today. */
  day?: string;
  /** A newer GET /v1/closures answer, in place of the pack's closures block. */
  closures?: ClosuresResponse;
}

/** POST it to /route on your own Valhalla, the pack's tiles.tar as mjolnir.tile_extract. */
export interface PackRouteBody {
  locations: {
    lon: number;
    lat: number;
  }[];
  costing: string;
  costing_options: Record<string, unknown>;
  exclude_polygons: LonLat[][];
  units: 'km';
}

/** What packRoute() returns: the Valhalla request and what to show with the route. */
export interface PackRoute {
  body: PackRouteBody;
  /** The manifest's id. */
  pack: string;
  day: string;
  /** The closures the body avoids, in list order. Show them with the route. */
  closures: string[];
  /** The pack's route costs come from trace scores. */
  scored: boolean;
  confidence: string;
  /**
   * What confidence means, for the driver: the manifest's confidence_note and confidence_note_mn when it has both;
   * else PACK_TRACE_NOTE and PACK_TRACE_NOTE_MN when the pack is not scored; else null.
   */
  confidence_note: string | null;
  confidence_note_mn: string | null;
  /** Show it with every route. */
  attribution: string;
  note: string;
  /** Say it to the driver when the pack has no trace scores: confidence_note, en and mn; null when scored. */
  trace_note: string | null;
  trace_note_mn: string | null;
}

/** A stop inside a closure ring: Valhalla has no road there. Refuse the route, as GET /v1/route does (422 end_in_closure). */
export class EndInClosureError extends Error {
  /** The stop's index in `stops`. */
  readonly stop: number;
  /** The closure's id. */
  readonly closure: string;
  constructor(stop: number, closure: string);
}

/** What to say when a pack has no trace scores (confidence osm_only), in English. */
export const PACK_TRACE_NOTE = "No trace scores yet: the route follows OSM roads and tracks, not checked by drivers.";

/** The same in Mongolian. */
export const PACK_TRACE_NOTE_MN = "\u0410\u044F\u043B\u043B\u044B\u043D \u043C\u044D\u0434\u044D\u044D\u043B\u044D\u043B \u0445\u0430\u0440\u0430\u0430\u0445\u0430\u043D \u0430\u043B\u0433\u0430: \u043C\u0430\u0440\u0448\u0440\u0443\u0442 OSM-\u0438\u0439\u043D \u0437\u0430\u043C\u0430\u0430\u0440, \u0436\u043E\u043B\u043E\u043E\u0447 \u0448\u0430\u043B\u0433\u0430\u0430\u0433\u04AF\u0439.";

/**
 * Offline: the Valhalla request for a route through `stops` (2 or more [lon, lat]) on the pack: the manifest's
 * costing and costing_options (copied), the route day's closure rings as exclude_polygons, units km. Throws
 * RangeError for a manifest with no request costing or bbox, fewer than 2 stops, a stop that is not [lon, lat] or is
 * outside the pack's bbox; excludePolygons' errors for the day; EndInClosureError for a stop inside an avoided ring.
 */
export function packRoute(manifest: PackRouteManifest, stops: LonLat[], options?: PackRouteOptions): PackRoute;

pois.ts ​

ts
/** A fuel station or a ger camp. */
export type PoiKind = 'fuel' | 'ger_camp';

/** One registered place in an offline pack's pois.geojson. Unregistered places are never in the file. */
export interface PoiFeature {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: {
    kind: PoiKind;
    /** The OSM element, N or W and the id, as N123. */
    osm: string;
    /** Clean name from the state registry match. */
    name: string;
    name_en: string | null;
    /** State registration number. */
    reg_number: string;
  };
}

/** Per kind, all Mongolia. */
export type PoiCounts = Record<PoiKind, {
  registered: number;
  unregistered: number;
}>;

/** An offline pack's pois.geojson (files[2]; index.json pois_sha256). Check its sha256 against the manifest before use. */
export interface PackPois {
  type: 'FeatureCollection';
  /** False: the registry could not be read when the pack was built, so no place is listed. */
  registry_checked: boolean;
  counts: PoiCounts;
  written: number;
  rule: string;
  rule_mn: string;
  source: string;
  note: string;
  note_mn: string;
  features: PoiFeature[];
}

/** A registered fuel station or ger camp near the route. */
export interface PoiOnRoute {
  kind: PoiKind;
  point: LonLat;
  osm: string;
  name: string;
  name_en: string | null;
  reg_number: string;
  /** Along the route to its point nearest the place, m, rounded. A tie goes to the earlier pass. */
  along_m: number;
  /** From the route line, m, rounded. */
  off_m: number;
}

/** What poisOnRoute() returns. */
export interface RoutePois {
  near_m: number;
  /** Registered places only, in route order. */
  pois: PoiOnRoute[];
  /** False: no place was listed because the registry was not read; say so, do not say "none". */
  registry_checked: boolean;
  /** Unregistered places in the pack, all Mongolia, not near this route: the pack has no place for them. An empty
   * list is not "no fuel on the route" while these are above 0. */
  unlisted: Record<PoiKind, number>;
  /** Show with the places: from OSM, not field checked; unregistered places are counted, not shown. */
  note: string;
  note_mn: string;
}

/** lane D's route pages: fuel and ger camps within 2 km of the line. */
export const POI_NEAR_M = 2000;

/**
 * Offline: the registered fuel stations and ger camps within `nearM` (default 2000) of a route line, from an offline
 * pack's pois.geojson, in route order. Throws RangeError for a line under 2 points or a nearM not above 0.
 */
export function poisOnRoute(pois: PackPois, line: LonLat[], nearM?: number): RoutePois;

passes.ts ​

ts
/** OSM mountain_pass=yes or natural=saddle. */
export type PassKind = 'mountain_pass' | 'saddle';

/** One pass in an offline pack's passes.geojson: an OSM mountain_pass=yes or natural=saddle node of a car way. */
export interface PassFeature {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: {
    /** mountain_pass when OSM has that tag, else saddle. */
    kind: PassKind;
    name: string | null;
    /** The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. Absent
     * when name is the OSM text. Show name. Packs before offline v12 lack it. */
    name_osm?: string;
    name_en: string | null;
    /** The OSM ele tag read as metres. Null when OSM has none, or only other text, as "7000 ft": see ele_osm. */
    ele_m: number | null;
    /** The OSM ele tag as written. */
    ele_osm: string | null;
    /** The OSM node id. */
    node_id: number;
    /** The Copernicus GLO-30 DEM height of the cell the node falls in, whole metres: a second source beside ele_m,
     * never mixed into it. Null when the pack had no DEM tile there; packs built before 2026-10-03 lack it. */
    dem_m?: number | null;
    /** True when ele_m and dem_m are both there and more than dem.differ_m (50 m) apart, false within, null when
     * either is missing. True says one of the two is wrong, not which. Packs built before passes v3 lack it. */
    heights_differ?: boolean | null;
    /** The OSM car ways through the node. Empty on a new track alone. */
    route_way_ids: number[];
    new_track: boolean;
    /** The ways' OSM highway and surface tags, as on crossings. Null on a new track alone. */
    road: CrossingRoad | null;
  };
}

/** The pack's DEM block: where dem_m comes from. Show attribution wherever a DEM height shows. */
export interface PassDem {
  source: string;
  attribution: string;
  rule: string;
  rule_mn: string;
  note: string;
  note_mn: string;
  /** DEM tiles read. */
  tiles: number;
  /** Tiles the build had no copy of: passes there have dem_m null. */
  missing: string[];
  /** Heights further apart than this, m, set heights_differ. Packs before passes v3 lack it and the three below. */
  differ_m?: number;
  differ_rule?: string;
  differ_rule_mn?: string;
  /** Passes with heights_differ true, all Mongolia. */
  differ?: number;
}

/** An offline pack's passes.geojson (manifest files row and passes block; index.json passes_sha256). Check its sha256
 * against the manifest before use. Packs built before 2026-10-03 have none. */
export interface PackPasses {
  type: 'FeatureCollection';
  /** Passes in the file, all Mongolia. */
  written: number;
  /** Per kind, all Mongolia. */
  counts: Partial<Record<PassKind, number>>;
  named: number;
  /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. Packs before offline v12 lack
   * them. */
  names_folded?: number;
  name_rule?: string;
  name_rule_mn?: string;
  /** Passes with a height in metres. */
  with_ele: number;
  /** Passes with a DEM height. Packs built before 2026-10-03 lack it. */
  with_dem?: number;
  /** Packs built before 2026-10-03 lack it. */
  dem?: PassDem;
  /** A route goes over a pass when its line comes this close, m. */
  match_m: number;
  rule: string;
  rule_mn: string;
  source: string;
  note: string;
  note_mn: string;
  features: PassFeature[];
}

/** A pass the route goes over. */
export interface PassOnRoute {
  kind: PassKind;
  point: LonLat;
  name: string | null;
  /** The OSM text when name was folded to one script, else null. Show name. */
  name_osm: string | null;
  name_en: string | null;
  ele_m: number | null;
  ele_osm: string | null;
  node_id: number;
  /** The DEM height, whole metres, beside ele_m. Null when the pack has none for this pass. Label it DEM. */
  dem_m: number | null;
  /** The pack's flag: true when ele_m and dem_m disagree by more than dem.differ_m. Null when either is missing or
   * the pack has no flag. Show both heights and the dem differ_rule when true. */
  heights_differ: boolean | null;
  new_track: boolean;
  road: CrossingRoad | null;
  /** Along the route to its point nearest the pass, m, rounded. A tie goes to the earlier pass. */
  along_m: number;
  /** From the route line, m, rounded to 0.1. */
  off_m: number;
}

/** What passesOnRoute() returns. */
export interface RoutePasses {
  match_m: number;
  /** In route order. */
  passes: PassOnRoute[];
  /** The highest ele_m of these passes, m. Null when none has a height: not "flat", OSM has no number. */
  top_m: number | null;
  /** The highest dem_m of these passes, m. Null when none has one. Not mixed with top_m. */
  top_dem_m: number | null;
  /** How many of these passes have heights_differ true. Null when the pack has no flag (no dem.differ_rule): not
   * checked, not 0. */
  differ: number | null;
  /** The pack's DEM block, null when the pack has none. Show its attribution with a DEM height. */
  dem: PassDem | null;
  /** Show with the passes: from OSM tags, not field checked. */
  note: string;
  note_mn: string;
  rule: string;
  rule_mn: string;
  /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. Null when the pack has no rule. */
  name_rule: string | null;
  name_rule_mn: string | null;
}

/**
 * Offline: the OSM mountain passes a route line goes over, from an offline pack's passes.geojson, in route order. A
 * route goes over a pass when its line comes within the pack's match_m (5 m) of the pass node, as lane D's route
 * pages. Throws RangeError for a line under 2 points.
 */
export function passesOnRoute(passes: PackPasses, line: LonLat[]): RoutePasses;

places.ts ​

ts
/** Default number of places kept. The least picked, then the longest unpicked, go first. */
export const PLACES_MAX = 100;

/** Key in the partner's store. A new format gets a new key. */
export const PLACES_KEY = "dock.places.v1";

/** localStorage or anything with its three calls (a native key-value bridge, a test map). */
export interface PlaceStore {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}

/** ClientOptions.placeCache: where picked places are kept, and how many. */
export interface PlaceCacheOptions {
  /** 1 to 1000. Default 100. */
  max?: number;
  /** Keeps the places across page loads. Default none: memory only, gone with the page. */
  store?: PlaceStore;
}

/** A geocode answer. `offline` is true only when it came from this device's picked places, not the API. */
export type GeocodeAnswer = GeocodeResponse & {
  offline?: true;
};

map.ts ​

ts
/** A saved style id: "st_" and 10 characters of the key alphabet. */
export const STYLE_ID_RE: RegExp;

/**
 * What createMap needs from maplibre-gl 6.4.1 or later (earlier versions carry XSS GHSA-jrc7-96c5-q579). Pass the
 * module: `import * as maplibregl from 'maplibre-gl'` (6 has no default export). Under a bundler it also needs
 * `maplibregl.setWorkerUrl(...)`: see the SDK page, Bundlers.
 */
export interface MapLibreLike {
  Map: new (options: any) => any;
}

/** What createMap() takes. Any option not named here goes to the maplibre-gl Map as is. */
export interface MapOptions extends StyleOptions {
  container: string | HTMLElement;
  center?: [number, number];
  zoom?: number;
  /** A full style to use instead of dockStyle. Add dockSources and dockLayers to it yourself. */
  style?: unknown;
  /** A style saved with POST /v1/styles or the style editor ("st_" and 10 characters). Loaded from the API. */
  styleId?: string;
  /** Runs after the key is added, for your own hosts. */
  transformRequest?: (url: string, resourceType?: string) => RequestParameters | undefined;
  /** Keep the two finger twist that turns the map. Off by default: a pinch must not turn it. */
  rotate?: boolean;
  /** Anything else goes to the maplibre-gl Map constructor as is. */
  [option: string]: unknown;
}

/** A MapLibre map on dock tiles, keyed through the client. Attribution comes from the TileJSON. */
export function createMap(maplibregl: MapLibreLike, client: Client, options: MapOptions): any;

style.ts ​

ts
/** Ulaanbaatar, Sükhbaatar Square. */
export const UB_CENTER: [number, number];

/** Colors and extrusion for dockStyle(), dockLayers() and createMap(). */
export interface StyleOptions {
  /** Background color. Default a light paper tone. */
  background?: string;
  /** Building fill (or extrusion) color. Default a warm grey. */
  building?: string;
  /** Building outline color. */
  buildingOutline?: string;
  /** Extrude buildings by height_m (or levels x 3 m). Default false: flat 2D. */
  extrude?: boolean;
}

/** Sources named dock-building and dock-entrance, TileJSON from the API. The key is added by transformRequest. */
export function dockSources(baseUrl: string): Record<string, Json>;

/**
 * Building fill and outline, entrance dots by confidence. Add on top of any basemap style.
 * Entrance colors are fixed: confidence reads the same on every map (PLAN.md 5). The API draws saved styles with the
 * same layers (api/src/modules/styles/style-doc.ts); a test keeps them equal.
 */
export function dockLayers(options?: StyleOptions): Json[];

/** A whole style: background, dock buildings, dock entrances. */
export function dockStyle(baseUrl: string, options?: StyleOptions): Json;

isochrone.ts ​

ts
/** The source id isochroneLayers() draws by default. */
export const ISOCHRONE_SOURCE = "dock-isochrone";

/** Which source to draw and how. */
export interface IsochroneLayerOptions {
  /** The source id given to map.addSource. Layer ids are this with "-fill" and "-line". Default "dock-isochrone". */
  source?: string;
  /** Fill and outline color. Default a dark teal. */
  color?: string;
  /** Fill opacity of one zone, 0 to 1. Zones overlap, so the nearest is the darkest. Default 0.2. */
  opacity?: number;
}

/**
 * A GeoJSON source from isochrone()'s answer: the features only, largest contour first, so each nearer zone is drawn
 * on top. Attribution is the answer's source (Valhalla, OpenStreetMap contributors).
 */
export function isochroneSource(isochrone: Isochrone): Json;

/** A fill and an outline for an isochroneSource(), drawn in feature order. Add them above dockLayers(). */
export function isochroneLayers(options?: IsochroneLayerOptions): Json[];

matching.ts ​

ts
/** The source id matchingLayers() draws by default. */
export const MATCHING_SOURCE = "dock-matching";

/** Which source to draw and how. */
export interface MatchingLayerOptions {
  /** The source id given to map.addSource. Layer ids are this with "-line" and "-point". Default "dock-matching". */
  source?: string;
  /** Line color, and the ring around each matched point. Default a blue. */
  color?: string;
  /** Line width in pixels, more than 0. Default 4. */
  width?: number;
}

/**
 * A GeoJSON source from matching()'s answer: one LineString per matching (matchings_index, distance_m, duration_s),
 * then one Point per tracepoint with a road (point, its index in the trace sent; matchings_index; gap_m). A null
 * tracepoint has no place on the road and is left out. Attribution is the answer's source.
 */
export function matchingSource(match: Match): Json;

/** A line for the matched roads and a dot for each matched point, on one matchingSource(). Add them above dockLayers(). */
export function matchingLayers(options?: MatchingLayerOptions): Json[];

trip.ts ​

ts
/** The source id optimizedTripLayers() draws by default. */
export const OPTIMIZED_TRIP_SOURCE = "dock-optimized-trip";

/** Which source to draw and how. */
export interface OptimizedTripLayerOptions {
  /** The source id given to map.addSource. Layer ids are this with "-line", "-stop" and "-number". Default "dock-optimized-trip". */
  source?: string;
  /** Line color, the ring around each stop and its number. Default a blue. */
  color?: string;
  /** Line width in pixels, more than 0. Default 4. */
  width?: number;
  /**
   * A font stack on the style's glyphs. Given: each stop shows its number in the order to drive, 1 first. Not given:
   * rings only, as dockStyle() has no glyphs.
   */
  font?: string[];
}

/**
 * A GeoJSON source from optimizedTrips()'s answer: one LineString per trip (trips_index, distance_m, duration_s), then
 * one Point per stop at its snapped place, in the order to drive (number, its place in the trip from 1; stop, its index
 * in the list sent; gap_m). Attribution is the answer's source. Not such an answer: TypeError. waypoint_index not 0 to
 * n - 1 once each: RangeError, as the numbers would be wrong.
 */
export function optimizedTripSource(round: OptimizedTrip): Json;

/** A line for the trip and a ring for each stop on one optimizedTripSource(), with `font` its number too. Add them above dockLayers(). */
export function optimizedTripLayers(options?: OptimizedTripLayerOptions): Json[];
ts
/** Off the line by more than this, NAV_OFF_FIXES fixes in a row: a new route from where the car is. */
export const NAV_OFF_M = 40;

/** Fixes in a row off the line before a reroute: one bad GPS fix never reroutes. */
export const NAV_OFF_FIXES = 3;

/** Within this of the road's end: arrived. */
export const NAV_ARRIVE_M = 20;

/** Where the car is on the route after one fix. */
export interface NavState {
  /** The step being driven (the last turn passed), by place in steps. */
  step: number;
  /** The next turn, or null after the last. */
  next: RouteStep | null;
  /** Metres along the road to the next turn. */
  toNext_m: number;
  /** Metres and seconds along the road to the end, the seconds at the route's own pace (expected_duration_s when the API gave it, else free-flow). */
  left_m: number;
  left_s: number;
  /** Metres off the line at this fix. */
  off_m: number;
  /** Rerouting: off the line for NAV_OFF_FIXES fixes, a new route asked for. */
  rerouting: boolean;
  arrived: boolean;
}

/** What a Navigator says and how it gets a new route. */
export interface NavigatorOptions {
  /** Phrase ids to say, in order (VoicePlayer.say). */
  onSay?: (ids: string[]) => void;
  /** A new route from `from` to the same end. Left out: no rerouting, the state says off_m. */
  reroute?: (from: LonLat) => Promise<Route>;
  /** Called with each new route after a reroute. */
  onRoute?: (route: Route) => void;
}

/** The phrase id for a distance ahead: the largest bucket at or under it. */
export function aheadPhrase(m: number): string | null;

/** Follows one route fix by fix. update() is cheap; call it with every position. */
export class Navigator {
  /** The route being driven; a new one after each reroute. */
  route: Route;
  /** Its steps, every leg's in order. */
  steps: RouteStep[];
  constructor(route: Route, opts?: NavigatorOptions);
  /** One position fix, [lon, lat]. */
  update(pos: LonLat): NavState;
}

/** Plays phrase ids from a phrase set (PWA /voice/mn/phrases.json): one after another, never over each other. On by default. */
export class VoicePlayer {
  /** Voice on by default (Gantulga, 2026-10-12); false keeps it quiet. */
  enabled: boolean;
  /** `base` ends with / and holds phrases.json and the audio; `play` plays one URL to its end (an Audio element in a browser). */
  constructor(base: string, play?: (url: string) => Promise<void>);
  /** Reads phrases.json. Ids it does not have are skipped when said. */
  load(fetcher?: (url: string) => Promise<{
    json(): Promise<unknown>;
  }>): Promise<string[]>;
  /** Queues phrase ids to play after what is playing; nothing when off. */
  say(ids: string[]): void;
}

routemap.ts ​

ts
/** The source id routeLayers() draws by default. */
export const ROUTE_SOURCE = "dock-route";

/** Which source to draw and how. */
export interface RouteLayerOptions {
  /** The source id given to map.addSource. Layer ids are this with "-line", "-end" and "-number". Default "dock-route". */
  source?: string;
  /** Line color, the ring around each end and its number. Default a blue. */
  color?: string;
  /** Line width in pixels, more than 0. Default 4. */
  width?: number;
  /**
   * A font stack on the style's glyphs. Given: each end shows its number in the order passed, from 1. Not given:
   * rings only, as dockStyle() has no glyphs.
   */
  font?: string[];
}

/**
 * A GeoJSON source from route()'s answer: the line (mode, distance_m, duration_s), then one Point per end at its
 * snapped place, in the order passed: from, each via, to (number, from 1; role, "from", "via" or "to"; via, the via
 * end's index in the list sent, null for from and to; gap_m). An answer from an API before 0.67.0 has no via: from
 * and to only. Attribution is the answer's source. Not such an answer: TypeError.
 */
export function routeSource(route: Route): Json;

/** A line for the route and a ring for each end on one routeSource(), with `font` its number too. Add them above dockLayers(). */
export function routeLayers(options?: RouteLayerOptions): Json[];

format.ts ​

ts
/** Mongolian or English. */
export type Lang = 'mn' | 'en';

/**
 * What to show a person for a door or a place: the spoken form, then the name, then the long form, then the
 * code, each in `lang` first. Never the id. Anything without a matched official address ends in
 * "(албан бус)" / "(unofficial)": VISION rule 1, PLAN 4d.
 */
export function label(item: Door | Place | RouteDoor, lang?: Lang): string;

suggest.ts ​

ts
/** PLAN 18: "suggestions from the third character". */
export const SUGGEST_MIN_CHARS = 3;

/** Wait after the last key before asking. Under the 250 ms people notice, over most gaps between keys. */
export const SUGGEST_DELAY_MS = 200;

/** A door read from a typed code, or a geocode result. Doors have `entrance`; geocode results have `kind`. */
export type Suggestion = Door | GeocodeResult;

/** The calls the widget makes. A Client has them. */
export interface SuggestClient {
  geocode(q: string, options: {
    limit?: number;
    proximity?: LonLat;
    types?: PlaceKind[];
    signal?: AbortSignal;
  }): Promise<{
    results: GeocodeResult[];
  }>;
  door(code: string, options: {
    unit?: string;
    signal?: AbortSignal;
  }): Promise<Door>;
  pick(result: GeocodeResult): void;
}

/** What a Suggester takes. */
export interface SuggestOptions {
  /** Geocode results per list, 1 to 20. Default 5. A door from a typed code comes on top of them. */
  limit?: number;
  /** [lon, lat] of the person or the map centre, or a function asked on each request. Nearer places first. */
  proximity?: LonLat | (() => LonLat | null | undefined);
  /**
   * Only these kinds, as geocode types: a street picker asks ['street']. Absent or empty: every kind. A
   * typed code or a pasted door link is looked up as a door only when types has door. Another kind is refused by the
   * constructor with the API's 400 invalid_types: no request.
   */
  types?: PlaceKind[];
  /** Default 3. */
  minChars?: number;
  /** Default 200 ms. */
  delayMs?: number;
}

/**
 * Text in, suggestions out, with no DOM. `input(text)` on every change; `show` gets the list for the text as it
 * stands, never an older one. Under minChars the list is emptied at once and nothing is sent. A text that reads as
 * a building code (parseDoorCode) is looked up as a door first; a code with no door falls through to the words. A
 * pasted door link (parseDoorLink) is looked up as a door only.
 */
export class Suggester {
  constructor(client: SuggestClient, options: SuggestOptions, show: (items: Suggestion[], q: string) => void, fail: (e: unknown) => void);
  /** The input now reads text. From minChars characters on, asks delayMs after the last call; under them, empties the list. */
  input(text: string): void;
  /** Stops the wait and the request in flight. Their answers are dropped. */
  cancel(): void;
}

/** What attachSuggest() takes. */
export interface AttachSuggestOptions extends SuggestOptions {
  /** The person chose a suggestion. The input already shows its label. A building code lookup gives a Door. */
  onPick: (item: Suggestion) => void;
  /** A DockError or a network failure. Default: the list is emptied. */
  onError?: (e: unknown) => void;
  /** Labels and the no-match line. Default 'mn'. */
  lang?: Lang;
  /** Where the options go. Default: a new <ul> right after the input. */
  list?: HTMLElement;
}

/** What attachSuggest() returns. */
export interface SuggestBox {
  readonly list: HTMLElement;
  /** Empties the list and stops any request. */
  clear(): void;
  /** Removes the listeners, and the list if attachSuggest made it. */
  destroy(): void;
}

/**
 * One address input for a checkout: as the person types, places and doors appear under it; arrows and Enter or a
 * click choose one. The chosen GeocodeResult goes to client.pick (offline places, opted-in telemetry), then onPick.
 * Each option reads label(item, lang), so anything without a matched official address says so. No styles: the list
 * has class dock-suggest, the empty line dock-suggest-empty, the active option aria-selected="true".
 */
export function attachSuggest(input: HTMLInputElement, client: SuggestClient, options: AttachSuggestOptions): SuggestBox;

searchbox.ts ​

ts
/** The pick's mark on the map: a source and a circle layer of this id. */
export const SEARCH_SOURCE = "dock-search";

/** What a SearchBox takes. */
export interface SearchBoxOptions {
  /** Labels, card and placeholder in 'mn' (default) or 'en'. */
  lang?: Lang;
  placeholder?: string;
  /** Called with each pick, after the map moved. */
  onPick?: (item: Suggestion) => void;
  /** Zoom the map flies to on a pick. Default 17. */
  zoom?: number;
  /** Mark colour. */
  color?: string;
}

/** The map as the control uses it: maplibre-gl's Map, or anything with these. */
export interface SearchBoxMap {
  getCenter(): {
    toArray(): number[];
  };
  flyTo(o: {
    center: [number, number];
    zoom: number;
  }): unknown;
  getSource(id: string): unknown;
  addSource(id: string, s: unknown): unknown;
  addLayer(l: unknown): unknown;
  getLayer(id: string): unknown;
  removeLayer(id: string): unknown;
  removeSource(id: string): unknown;
  isStyleLoaded?(): boolean | void;
  once?(event: string, f: () => void): unknown;
}

/** A MapLibre control (IControl): map.addControl(new SearchBox(client, options), 'top-left'). */
export class SearchBox {
  /** The control's element, once added. */
  container: HTMLElement | null;
  /** The last pick, or null. */
  picked: Suggestion | null;
  constructor(client: SuggestClient, options?: SearchBoxOptions);
  /** maplibre-gl calls it on map.addControl: builds the field, its list and the card. */
  onAdd(map: SearchBoxMap): HTMLElement;
  /** maplibre-gl calls it on map.removeControl: the mark, the card and the listeners go. */
  onRemove(): void;
  /** Takes the card and the mark away. */
  clear(): void;
}

telemetry.ts ​

ts
/** One search as the API takes it. Text, count and pick only: no time, point, device or session. */
export interface SearchEvent {
  q: string;
  results: number;
  picked?: {
    kind: PlaceKind;
    id: string;
    rank: number;
  };
}

trace.ts ​

ts
/** API limits (api/src/modules/traces/trace-input.ts). */
export const TRACE_FORMAT = "dock.trace.v1";

/** Points per upload. stopTrace() splits a longer trip. */
export const TRACE_MAX_POINTS = 20000;

/** [lon, lat, t]: degrees WGS84 and whole seconds UTC. Nothing else per point. */
export type TracePoint = [number, number, number];

/** The part of a browser GeolocationPosition the recorder reads. */
export interface PositionFix {
  coords: {
    longitude: number;
    latitude: number;
    accuracy?: number | null;
  };
  timestamp: number;
}

/** navigator.geolocation, or anything with the same two calls (a native bridge, a test). */
export interface PositionSource {
  watchPosition(success: (fix: PositionFix) => void, error?: ((err: unknown) => void) | null, options?: PositionOptions): number;
  clearWatch(id: number): void;
}

probe.ts ​

ts
/** API limits (api/src/modules/traces/traffic.ts). */
export const PROBE_FORMAT = "dock.probe.v1";

/** Points per batch. Older points are dropped first when a send keeps failing. */
export const PROBE_MAX_POINTS = 900;

/** Seconds between sends. */
export const PROBE_SEND_S = 30;

/** The API's window: the vehicle token is made anew in each. */
export const PROBE_WINDOW_S = 900;

/** 24 random characters: a vehicle token. Never a plate, phone or device id. */
export function probeToken(): string;

/** One batch for POST /v1/traffic/probes: the format, the window's vehicle token and the points. Nothing else. */
export interface ProbeBody {
  format: typeof PROBE_FORMAT;
  vehicle: string;
  points: TracePoint[];
}

/**
 * One vehicle's points since the last send, in memory only. One point per second at most; a fix that goes back in
 * time, is outside Mongolia, or is less accurate than `maxAccuracyM` is left out. The last point sent opens the next
 * batch, so the segment across a send is counted once. The token changes with the 15-minute window.
 */
export class ProbeBuffer {
  constructor(maxAccuracyM?: number, token?: () => string);
  /** Points waiting to be sent. */
  get size(): number;
  /** true when the fix was kept. */
  add(fix: PositionFix): boolean;
  /** The batch to send now, or null under 2 points. The points leave the buffer; the last one opens the next batch. */
  take(): ProbeBody | null;
  /** Drops every point held, the carried one too. */
  clear(): void;
}

event.ts ​

ts
/** API limits (api/src/modules/traces/events.ts). */
export const EVENT_POSITION_FORMAT = "dock.position.v1";

/** Points per batch. Older points are dropped first when a send keeps failing. */
export const EVENT_MAX_POINTS = 900;

/** Seconds between sends: the live page shows points of the last 2 minutes. */
export const EVENT_SEND_S = 10;

/** One batch for POST /v1/events/{id}/positions: the format, consent, the participant token and the points. Nothing else. */
export interface EventPositionBody {
  format: typeof EVENT_POSITION_FORMAT;
  consent: 'opt_in';
  participant: string;
  points: TracePoint[];
}

/**
 * One participant's points since the last send, in memory only. One point per second at most; a fix that goes back
 * in time, is outside Mongolia, or is less accurate than `maxAccuracyM` is left out. The participant token is random,
 * made once per buffer: never a name, bib, phone or device id.
 */
export class EventBuffer {
  /** The random participant token sent with every batch of this run. */
  readonly participant: string;
  constructor(maxAccuracyM?: number, token?: () => string);
  /** Points waiting to be sent. */
  get size(): number;
  /** true when the fix was kept. */
  add(fix: PositionFix): boolean;
  /** The batch to send now, or null when empty. The points leave the buffer. */
  take(): EventPositionBody | null;
}

sand.ts ​

ts
/** One stretch in an offline pack's sand.geojson: the part of one car way inside an OSM natural=sand area. */
export interface SandFeature {
  type: 'Feature';
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  };
  properties: {
    /** The area's OSM element, as way/123 or relation/123. */
    area: string;
    /** The area's OSM name. */
    name: string | null;
    /** The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. Absent
     * when name is the OSM text. Show name. Packs before offline v11 lack it. */
    name_osm?: string;
    /** The stretch's length, km. */
    km: number;
    /** The car way's OSM id. Null on a new track alone. */
    way_id: number | null;
    new_track: boolean;
    /** The way's OSM highway and surface tags, as on crossings. Null on a new track alone. */
    road: CrossingRoad | null;
  };
}

/** An offline pack's sand.geojson (manifest files row and sand block; index.json sand_sha256). Check its sha256
 * against the manifest before use. Packs built before 2026-10-03 have none. */
export interface PackSand {
  type: 'FeatureCollection';
  /** Stretches in the file, all Mongolia. */
  written: number;
  km: number;
  /** OSM sand areas the stretches lie in. */
  areas: number;
  /** Stretches in a named area. */
  named: number;
  /** Stretches on a paved road (road.paved true), as town streets drawn inside sand. */
  on_paved: number;
  /** A route runs on a stretch where its line comes this close, m. */
  match_m: number;
  rule: string;
  rule_mn: string;
  source: string;
  /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. Packs before offline v11
   * lack them. */
  names_folded?: number;
  name_rule?: string;
  name_rule_mn?: string;
  note: string;
  note_mn: string;
  features: SandFeature[];
}

/** A stretch through OSM sand the route runs on. */
export interface SandOnRoute {
  area: string;
  name: string | null;
  /** The OSM text when name was folded to one script, else null. Show name. */
  name_osm: string | null;
  /** The whole stretch, km, from the pack. The route may run on part of it: on_m. */
  km: number;
  way_id: number | null;
  new_track: boolean;
  road: CrossingRoad | null;
  /** Along the route to where it meets the stretch, m, rounded. */
  along_m: number;
  /** Metres of the stretch the route runs on, rounded. */
  on_m: number;
  /** The stretch's first point the route meets, in route order. */
  point: LonLat;
}

/** What sandOnRoute() returns. */
export interface RouteSand {
  match_m: number;
  min_on_m: number;
  /** In route order. */
  stretches: SandOnRoute[];
  /** Sum of on_m, m. */
  on_m: number;
  /** Show with the stretches: from OSM tags, not field checked. */
  note: string;
  note_mn: string;
  rule: string;
  rule_mn: string;
  /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. Null when the pack has no rule. */
  name_rule: string | null;
  name_rule_mn: string | null;
}

/** A route that runs on less of a stretch than this, m, only crosses it, unless it runs on all of the stretch. */
export const SAND_MIN_ON_M = 20;

/**
 * Offline: the stretches through OSM sand a route line runs on, from an offline pack's sand.geojson, in route order.
 * The same rule as wetlandsOnRoute: the stretch is cut in pieces of at most match_m; a piece is on the route when both
 * its ends lie within match_m of the line. A stretch is on the route when its pieces on the route make at least
 * SAND_MIN_ON_M, or when all of it is on the route. Throws RangeError for a line under 2 points.
 */
export function sandOnRoute(sand: PackSand, line: LonLat[]): RouteSand;

water.ts ​

ts
/** The OSM tag the water point carries. */
export type WaterKind = 'drinking_water' | 'water_point' | 'water_tap' | 'water_well' | 'spring';

/** One water point in an offline pack's water.geojson: an OSM node or way, a way at a point on its surface. */
export interface WaterFeature {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: {
    /** The first of drinking_water, water_point, water_tap, water_well, spring the OSM element carries. */
    kind: WaterKind;
    name: string | null;
    /** The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. Absent
     * when name is the OSM text. Show name. Packs before offline v12 lack it. */
    name_osm?: string;
    name_en: string | null;
    /** Only what OSM says: true for drinking_water=yes (or amenity=drinking_water with no such tag), false for
     * drinking_water=no, null when OSM says nothing. Never guessed from kind. */
    drinkable: boolean | null;
    /** The OSM drinking_water tag as written. */
    drinking_water: string | null;
    /** The OSM pump tag as written. */
    pump: string | null;
    /** The OSM element, as node/1 or way/1. */
    osm: string;
  };
}

/** Water points per drinkable value. */
export interface WaterDrinkable {
  yes: number;
  no: number;
  /** OSM says nothing. */
  unknown: number;
}

/** An offline pack's water.geojson (manifest files row and water block; index.json water_sha256). Check its sha256
 * against the manifest before use. Packs built before 2026-10-03 have none. */
export interface PackWater {
  type: 'FeatureCollection';
  /** Water points in the file, all Mongolia. */
  written: number;
  /** Per kind, all Mongolia. */
  counts: Partial<Record<WaterKind, number>>;
  named: number;
  /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. Packs before offline v12 lack
   * them. */
  names_folded?: number;
  name_rule?: string;
  name_rule_mn?: string;
  /** All Mongolia. */
  drinkable: WaterDrinkable;
  /** A water point is near a route within this many metres of its line, as fuel and ger camps. */
  near_m: number;
  rule: string;
  rule_mn: string;
  near_rule: string;
  near_rule_mn: string;
  source: string;
  note: string;
  note_mn: string;
  features: WaterFeature[];
}

/** A water point near the route. */
export interface WaterOnRoute {
  kind: WaterKind;
  point: LonLat;
  name: string | null;
  /** The OSM text when name was folded to one script, else null. Show name. */
  name_osm: string | null;
  name_en: string | null;
  drinkable: boolean | null;
  drinking_water: string | null;
  pump: string | null;
  osm: string;
  /** Along the route to its point nearest the water, m, rounded. A tie goes to the earlier pass. */
  along_m: number;
  /** From the route line, m, rounded. */
  off_m: number;
}

/** What waterOnRoute() returns. */
export interface RouteWater {
  near_m: number;
  /** In route order. */
  water: WaterOnRoute[];
  /** Of these points. unknown: OSM says nothing, not "not drinkable". */
  drinkable: WaterDrinkable;
  /** Show with the points: from OSM tags, not field checked; a well may be dry, broken or locked. */
  note: string;
  note_mn: string;
  rule: string;
  rule_mn: string;
  near_rule: string;
  near_rule_mn: string;
  /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. Null when the pack has no rule. */
  name_rule: string | null;
  name_rule_mn: string | null;
}

/**
 * Offline: the OSM water points (wells, springs, taps, drinking water) within `nearM` of a route line, from an
 * offline pack's water.geojson, in route order. nearM defaults to the pack's near_m (2000), lane D's route pages.
 * Throws RangeError for a line under 2 points or a nearM not above 0.
 */
export function waterOnRoute(water: PackWater, line: LonLat[], nearM?: number): RouteWater;

wetlands.ts ​

ts
/** One stretch in an offline pack's wetlands.geojson: the part of one car way inside an OSM natural=wetland area. */
export interface WetlandFeature {
  type: 'Feature';
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  };
  properties: {
    /** The area's OSM wetland tag, as marsh, bog, wet_meadow or fen; unknown when the area has none. */
    wetland: string;
    /** The area's OSM element, as way/123 or relation/123. */
    area: string;
    /** The area's OSM name. */
    name: string | null;
    /** The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. Absent
     * when name is the OSM text. Show name. Packs before offline v12 lack it. */
    name_osm?: string;
    /** The stretch's length, km. */
    km: number;
    /** The car way's OSM id. Null on a new track alone. */
    way_id: number | null;
    new_track: boolean;
    /** The way's OSM highway and surface tags, as on crossings. Null on a new track alone. */
    road: CrossingRoad | null;
  };
}

/** An offline pack's wetlands.geojson (manifest files row and wetlands block; index.json wetlands_sha256). Check its
 * sha256 against the manifest before use. Packs built before 2026-10-03 have none. */
export interface PackWetlands {
  type: 'FeatureCollection';
  /** Stretches in the file, all Mongolia. */
  written: number;
  km: number;
  /** OSM wetland areas the stretches lie in. */
  areas: number;
  /** Per wetland kind, all Mongolia. */
  counts: Record<string, {
    stretches: number;
    km: number;
  }>;
  /** A route runs on a stretch where its line comes this close, m. */
  match_m: number;
  rule: string;
  rule_mn: string;
  source: string;
  /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. Packs before offline v12
   * lack them. */
  names_folded?: number;
  name_rule?: string;
  name_rule_mn?: string;
  note: string;
  note_mn: string;
  features: WetlandFeature[];
}

/** A stretch through OSM wetland the route runs on. */
export interface WetlandOnRoute {
  wetland: string;
  area: string;
  name: string | null;
  /** The OSM text when name was folded to one script, else null. Show name. */
  name_osm: string | null;
  /** The whole stretch, km, from the pack. The route may run on part of it: on_m. */
  km: number;
  way_id: number | null;
  new_track: boolean;
  road: CrossingRoad | null;
  /** Along the route to where it meets the stretch, m, rounded. */
  along_m: number;
  /** Metres of the stretch the route runs on, rounded. */
  on_m: number;
  /** The stretch's first point the route meets, in route order. */
  point: LonLat;
}

/** What wetlandsOnRoute() returns. */
export interface RouteWetlands {
  match_m: number;
  min_on_m: number;
  /** In route order. */
  stretches: WetlandOnRoute[];
  /** Sum of on_m, m. */
  on_m: number;
  /** Show with the stretches: from OSM tags, not field checked. */
  note: string;
  note_mn: string;
  rule: string;
  rule_mn: string;
  /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. Null when the pack has no rule. */
  name_rule: string | null;
  name_rule_mn: string | null;
}

/** A route that runs on less of a stretch than this, m, only crosses it, unless it runs on all of the stretch. */
export const WETLAND_MIN_ON_M = 20;

/**
 * Offline: the stretches through OSM wetland a route line runs on, from an offline pack's wetlands.geojson, in route
 * order. The stretch is cut in pieces of at most match_m; a piece is on the route when both its ends lie within
 * match_m of the line. A stretch is on the route when its pieces on the route make at least WETLAND_MIN_ON_M, or
 * when all of it is on the route. Throws RangeError for a line under 2 points.
 */
export function wetlandsOnRoute(wetlands: PackWetlands, line: LonLat[]): RouteWetlands;

types.ts ​

ts
/** [lon, lat], WGS84. */
export type LonLat = [number, number];

/** How far the place matched an official state address: fully, in part, or not at all (unofficial). */
export type OfficialStatus = 'matched' | 'partial' | 'none';

/** National format, as written by the source. */
export interface OfficialAddress {
  country: string;
  city_or_aimag: string;
  district_or_soum: string;
  khoroo_or_bag?: string;
  street_or_microdistrict?: string;
  building_or_khashaa?: string;
  entrance?: string;
  floor?: string;
  unit?: string;
  /** Unified postal code from the state register, "13381-0017". */
  postal_code?: string;
  /** The building's own name from the state register, when it has one. */
  building_name?: string;
}

/** The entrance a building code points to. */
export interface Entrance {
  kind: 'door' | 'gate' | 'vehicle_gate' | 'stairwell';
  /** Entrance or gate number as people say it. */
  letter: string | null;
  floors_served: number[] | null;
}

/** A flat, office or shop behind a door, when the code was given with a unit. */
export interface Unit {
  number: string;
  floor: number | null;
  kind: 'flat' | 'office' | 'shop' | 'stall' | 'kiosk';
}

/** A door. Official address first, code after it. No owner, no phone. */
export interface Door {
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  /** true unless official_status is matched. Show "unofficial" when true. */
  unofficial: boolean;
  official_source: string | null;
  official_date: string | null;
  /** The date of the door or unit row behind the code, YYYY-MM-DD. null: not known. Absent before API 0.87.0. */
  source_date?: string | null;
  code: string;
  natural_mn: string | null;
  natural_en: string | null;
  long_mn: string | null;
  long_en: string | null;
  point: LonLat;
  entrance: Entrance;
  unit: Unit | null;
  /** PLAN 5 level, 0 to 4. null means unknown. */
  confidence: number | null;
  /** Confirmed driver taps for this door since its point last moved. 0 when none. */
  confirmations?: number;
  source: string;
  instructions_mn: string | null;
  instructions_en: string | null;
  aliases: string[];
}

/** What a place is. */
export type PlaceKind = 'door' | 'business' | 'building' | 'district' | 'khoroo' | 'area' | 'street' | 'landmark';

/** A place in geocode, reverse and place(id) answers. Show unofficial unless official_status is matched. */
export interface Place {
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  unofficial: boolean;
  official_source: string | null;
  official_date: string | null;
  /** The official address as one Cyrillic line, postal code last. null when nothing is known. */
  official_line?: string | null;
  /** Building code. null when the place has none. */
  code: string | null;
  kind: PlaceKind;
  /** Stable id. Never show it to people as an address. */
  id: string;
  name_mn: string | null;
  name_en: string | null;
  long_mn: string | null;
  long_en: string | null;
  point: LonLat;
  confidence: number | null;
  source: string;
  /** The date of the row's current version, YYYY-MM-DD, or the register's date for a door. null: not known. Absent before API 0.87.0. */
  source_date?: string | null;
  /** A business: true when its company has a claim the state register confirmed, false when not or pending; absent for any other kind. Lane U hides the claim link when true. */
  claimed?: boolean;
  /** A business, landmark or street: the khoroo its point stands in. null when no polygon covers it; absent for any other kind. */
  khoroo?: number | null;
  /** A business: its point lies inside a restricted site, so hide the claim link and any edit. Absent for any other kind. */
  restricted?: boolean;
  /** A building with an indoor map: venue_id (GET /v1/venue/{id}), category (airport, mall, ...; API 0.102.0), levels (floor numbers, lowest first) and units, the units the query names with level, number, source and confidence (up to 5). Absent for every other row. */
  indoor?: {
    venue_id: string;
    category: string;
    levels: number[];
    units: IndoorUnitHit[];
  };
}

/** One geocode result. */
export interface GeocodeResult extends Place {
  /** Higher is better. Compare only with results of the same layer in one response. */
  score: number;
  /**
   * 1: the words of a name or address matched. 2: the place description matched the meaning; after every layer 1,
   * except near rows. 3: near rows reordered by a reranking model on the description (API 0.30.0); score is its, 0 to 1.
   */
  layer: 1 | 2 | 3;
  /** The landmark a landmark-relative query named ("Хүннү моллын ард"), for a place found around it. null otherwise. */
  near: GeocodeNear | null;
  /** A bus stop: the city bus lines that call at it ("Ч:41"), as nearby() gives them. null for every other place. */
  lines: string[] | null;
  /** A business or landmark: its map category, the one nearby() gives ("pharmacy"). null for any other place and when unknown. */
  category: string | null;
  /** A building: what it is, apartment, ger_house, commercial, public, industrial, garage or other. null when the building has no type yet; absent for any other kind. */
  building_type?: string | null;
  /** A building: its number of storeys (OSM building:levels), so a card can tell a tower from the podium that shares its name. null when unknown; absent for any other kind. */
  building_levels?: number | null;
  /** A building: its whole name is only a kind word ("Үсчин"), so show it as a kind, not a title. null when not checked; absent for any other kind. */
  name_is_kind_word?: boolean | null;
  /** A building: the OSM category of the one POI in its footprint (shop=hairdresser); null when unknown. */
  poi_category?: string | null;
  /** A building: up to 5 company names registered at it (register match only); [] when none; absent for any other kind. */
  poi_names?: string[];
  /** A company from a building's directory (API 0.90.0): its floor from the legal address in the state register. null when not said, and for every other place. */
  floor?: number | null;
  /** A company from a building's directory (API 0.90.0): its flat or office number (N тоот). null when not said. */
  flat?: string | null;
}

/** The landmark, building or door a near-me query was read against. */
export interface GeocodeNear {
  kind: 'landmark' | 'building' | 'door';
  id: string;
  name_mn: string | null;
  name_en: string | null;
  /** A side searches within 55 degrees of it; near and beside the whole circle. */
  relation: 'north' | 'east' | 'south' | 'west' | 'near' | 'beside';
  /** From the landmark to the place, 150 at most. */
  metres: number;
}

/** A unit code found in the query: the unit card as GET /v1/door/{code} answers it, plus the floor and flat spoken after the code. */
export interface GeocodeUnit {
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  /** true unless official_status is matched. Show "unofficial" when true. */
  unofficial: boolean;
  official_source: string | null;
  official_date: string | null;
  /** The date of the door or unit row behind the code, YYYY-MM-DD. null: not known. Absent before API 0.87.0. */
  source_date?: string | null;
  code: string;
  kind: 'building' | 'khashaa';
  /** The official address as one Cyrillic line, postal code last. null when nothing is known. */
  official_line: string | null;
  /** District and khoroo the building lies in, only when its address is not matched. Never the official address; unofficial stays true. null when matched or not known. */
  area_line?: string | null;
  /** What the building is: apartment, ger_house, commercial, public, industrial, garage or other. null for a khashaa or when not known yet; never guessed. */
  building_type?: string | null;
  /** Floors above ground from the building record. null for a khashaa or when not known; never guessed. */
  building_levels?: number | null;
  /** The OpenStreetMap opening_hours tag of the landmark inside the building, as written; a tag can be old. null for a khashaa or when none. */
  opening_hours?: string | null;
  hours_source?: string | null;
  name_mn: string | null;
  name_en: string | null;
  /** [lon, lat] of a point inside the footprint. */
  point: LonLat;
  /** N in "N давхар", null when not said. */
  floor: number | null;
  /** N in "N тоот", null when not said. */
  flat: string | null;
  /** The building holds an indoor map: its venue id (GET /v1/venue/{id}). Absent when it holds none. */
  venue_id?: string;
}

/** GET /v1/door/at: the address unit at a point, a хашаа first, else the smallest building that holds it. */
export interface UnitAt extends Omit<GeocodeUnit, 'code' | 'floor' | 'flat'> {
  /** null while the unit has no code yet. */
  code: string | null;
  /** Stable id of the unit. Never shown to people as an address. */
  id: string;
}

/** A unit inside a building with an indoor map, as a search row lists it (API 0.100.0). */
export interface IndoorUnitHit {
  unit_id: string;
  level: number;
  name_mn: string | null;
  number: string | null;
  category: string;
  source: string;
  confidence: number | null;
}

/** One unit the query names, as its own answer (API 0.100.0): the venue, its building code, the floor and a point inside the unit. */
export interface IndoorUnitAnswer {
  unit_id: string;
  venue_id: string;
  venue_name_mn: string | null;
  code: string | null;
  level: number;
  name_mn: string | null;
  number: string | null;
  category: string;
  point: LonLat | null;
  source: string;
  confidence: number | null;
}

/** One floor of a venue (API 0.100.0). `ordinal` is the floor as people say it: 0 ground, 1 first, -1 basement. */
export interface IndoorLevel {
  ordinal: number;
  short_name_mn: string;
  name_mn: string | null;
  outdoor: boolean;
  source: string;
  confidence: number | null;
}

/** GET /v1/venue/{id}: a building with an indoor map, its outline (GeoJSON Polygon, [lon, lat]) and its current floors, lowest first. */
export interface Venue {
  id: string;
  name_mn: string;
  name_en: string | null;
  category: string;
  source: string;
  confidence: number | null;
  /** The building code of the real-map building the venue stands in; null until it has one. */
  building_code: string | null;
  /** The district and khoroo covering the venue (API 0.101.0), and soum_mn and aimag_mn from the OSM admin polygons (API 0.103.0); null when no polygon covers it. */
  admin: {
    district_mn: string | null;
    district_en: string | null;
    khoroo: number | null;
    soum_mn: string | null;
    aimag_mn: string | null;
  } | null;
  outline: {
    type: 'Polygon';
    coordinates: number[][][];
  } | Record<string, unknown>;
  levels: IndoorLevel[];
}

/** One venue near a point (GET /v1/venue?near=lon,lat, API 0.105.0). `distance_m` is 0 when the point is inside the outline. */
export interface NearVenue {
  id: string;
  name_mn: string;
  category: string;
  distance_m: number;
  levels: number[];
}

/** GET /v1/venue?near=lon,lat: venues within 200 m, nearest first, at most 10. */
export interface NearVenues {
  results: NearVenue[];
}

/** One unit, opening or amenity on a floor. `properties.feature_type` says which; then category, level, source, confidence, and the names, number and area a unit has. */
export interface LevelFeature {
  type: 'Feature';
  id: string;
  geometry: Record<string, unknown>;
  properties: Record<string, unknown>;
}

/** GET /v1/venue/{id}/level/{n}: one floor as GeoJSON. */
export interface VenueLevel {
  type: 'FeatureCollection';
  venue_id: string;
  level: number;
  features: LevelFeature[];
}

/** GET /v1/venue/{id}/spot: what a printed plate for one unit says. `link` is what the plate's QR holds. */
export interface VenueSpot {
  venue_id: string;
  venue_name_mn: string;
  unit_id: string;
  level: number;
  level_name_mn: string;
  name_mn: string | null;
  number: string | null;
  label: string;
  point: LonLat;
  link: string;
  source: string;
  confidence: number | null;
}

/** One step of a walk inside a venue, in Mongolian, with the floor it is on. `at` is [lon, lat] where the step begins. */
export interface IndoorStep {
  floor: number;
  text: string;
  distance_m: number;
  at: LonLat;
}

/** One floor of a walk: its line (GeoJSON LineString, [lon, lat]) and how it ends: `stairs`, `escalator`, `elevator` or `arrive`; `to_floor` is the floor a connector reaches, null on arrival. */
export interface IndoorLeg {
  floor: number;
  distance_m: number;
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  } | Record<string, unknown>;
  ends_with: 'stairs' | 'escalator' | 'elevator' | 'arrive';
  to_floor: number | null;
}

/** GET /v1/venue/{id}/route: the walk inside a venue to one unit, floor by floor. `to` is the unit: level, point, name_mn, number, source, confidence. */
export interface VenueRoute {
  venue_id: string;
  distance_m: number;
  duration_s: number;
  legs: IndoorLeg[];
  steps: IndoorStep[];
  to: Record<string, unknown>;
}

/** GET /v1/venue/{id}/directions: the road to the street entrance, then the walk inside. Distance and duration are road plus walk. `road.entrance` holds level, point and core_entrance_id. */
export interface VenueDirections {
  venue_id: string;
  distance_m: number;
  duration_s: number;
  road: {
    mode: RouteMode;
    distance_m: number;
    duration_s: number;
    geometry: {
      type: 'LineString';
      coordinates: LonLat[];
    } | Record<string, unknown>;
    steps: {
      mn: string;
      en: string;
      distance_m: number;
      at: LonLat;
    }[];
    entrance: {
      level: number;
      point: LonLat;
      core_entrance_id: string | null;
    };
  };
  indoor: VenueRoute;
  /** Notes that do not block the walk; code entrance_closed when the entrance used is closed at arrival. */
  warnings: Record<string, unknown>[];
}

/** GET /v1/geocode. */
export interface GeocodeResponse {
  results: GeocodeResult[];
  /** Set when the whole query is a unit code, alone ("7QSVX") or spoken ("7QSVX, 12 давхар, 1204 тоот"). null otherwise and offline. */
  unit: GeocodeUnit | null;
  /** The units inside indoor-mapped buildings that the query names, each as its own answer; up to 5. Absent when none. */
  indoor_units?: IndoorUnitAnswer[];
}

/** One reverse result. */
export interface ReverseResult extends Omit<Place, 'kind'> {
  /** What it is. `khashaa` (API 0.93.8): a minted ger plot that holds the point, at distance 0, with its code and official line in long_mn and name_mn null; reverse gives it only when `types` is absent. */
  kind: PlaceKind | 'khashaa';
  /** Metres from the query point. 0 means the point is inside. */
  distance_m: number;
  /** A bus stop: the city bus lines that call at it ("Ч:41"), as nearby() gives them. null for every other place. */
  lines: string[] | null;
  /** A business or landmark: its map category, the one nearby() gives ("pharmacy"). null for any other place and when unknown. */
  category: string | null;
  /** A building: what it is, apartment, ger_house, commercial, public, industrial, garage or other. null when the building has no type yet; absent for any other kind. */
  building_type?: string | null;
  /** A building: its number of storeys (OSM building:levels), so a card can tell a tower from the podium that shares its name. null when unknown; absent for any other kind. */
  building_levels?: number | null;
  /** A building: its whole name is only a kind word ("Үсчин"), so show it as a kind, not a title. null when not checked; absent for any other kind. */
  name_is_kind_word?: boolean | null;
  /** A building: the OSM category of the one POI in its footprint (shop=hairdresser); null when unknown. */
  poi_category?: string | null;
  /** A building: up to 5 company names registered at it (register match only); [] when none; absent for any other kind. */
  poi_names?: string[];
}

/** A kind reverse can keep (types): district and khoroo always come with the answer. */
export type ReverseType = 'door' | 'business' | 'building' | 'landmark';

/** GET /v1/reverse. */
export interface ReverseResponse {
  point: LonLat;
  district_mn: string | null;
  district_en: string | null;
  /** null when no khoroo polygon covers the point. Never guessed. */
  khoroo: number | null;
  /** The soum and aimag covering the point (OSM boundaries, API 0.104.0); null when no polygon covers it. */
  soum_mn?: string | null;
  aimag_mn?: string | null;
  /** true when the point lies inside a restricted site (PLAN 20.2): results is then empty. */
  restricted?: boolean;
  results: ReverseResult[];
}

/**
 * A category nearby takes: the map's place icons. atm, bank, cafe, car_wash, fuel, hotel, pharmacy, restaurant and shop are registered
 * businesses only, so they answer nothing until a business matches the registry: a business through its door, or a
 * store of a registered chain (code null).
 */
export type NearbyCategory = 'atm' | 'bank' | 'bridge' | 'cafe' | 'car_wash' | 'culture' | 'embassy' | 'fuel' | 'government' | 'hospital' | 'hotel' | 'kindergarten' | 'mall' | 'market' | 'monument' | 'office' | 'park' | 'pharmacy' | 'religion' | 'restaurant' | 'school' | 'shop' | 'sport' | 'station' | 'stop' | 'university';

/** One nearby result. */
export interface NearbyResult extends Place {
  /**
   * Unit code of the place: a door, a business through its door, or the building or хашаа a store or company stands
   * in. null when none is minted yet: show "код алга" and the official address, which is the building's it stands in,
   * or none.
   */
  code: string | null;
  /** A registered company from a building's directory (API 0.92.1): its floor from the state register. null when not said, and for every other place. */
  floor: number | null;
  /** A company's flat or office number (N тоот) from the same address. null when not said. */
  flat: string | null;
  /** The OpenStreetMap opening_hours tag as written; may be old. null when the source has none. */
  opening_hours: string | null;
  /** Where opening_hours came from. null when opening_hours is null. */
  hours_source: string | null;
  /** Open now in Ulaanbaatar time, from a plain opening_hours tag (OpenStreetMap, may be old). null when unknown: no tag, or not a plain one. */
  open_now: boolean | null;
  /** When the place closes today, HH:MM in Ulaanbaatar time, while open_now is true. null when closed, unknown, or open 24/7. */
  open_until: string | null;
  /** When a closed place opens later today, HH:MM in Ulaanbaatar time. null when open, unknown, or nothing starts later today. */
  opens_at: string | null;
  /** Only when nearby() was asked for photo: one openly licensed photo of a landmark or building, as address() gives it. null when none. */
  photo?: {
    url: string;
    credit: string;
    license: string;
    source: string;
    author?: string;
  } | null;
  /** The map category, as the place icon. */
  category: NearbyCategory;
  /** Metres from the query point. */
  distance_m: number;
  /**
   * The city bus lines that call at this stop, as the bus shows them, letter then number ("Ч:41"). null when the
   * capital's public transport data lists none here.
   */
  lines: string[] | null;
}

/** GET /v1/nearby. */
export interface NearbyResponse {
  point: LonLat;
  /** Nearest first, within radius. Empty when none. */
  results: NearbyResult[];
  /** How many more places of these categories lie within radius past limit. 0 when results holds them all. */
  more: number;
  /** Only with open=1: how many of the nearest 20 places have no readable hours (open_now null). They are not in results. */
  hours_unknown?: number;
}

/** What reportDoor() sends: a door in the wrong place, missing, a duplicate, or not on the map. */
export type DoorReport = {
  kind: 'wrong_location';
  code: string;
  point: LonLat;
  note?: string;
} | {
  kind: 'missing';
  code: string;
  note?: string;
} | {
  kind: 'duplicate';
  code: string;
  duplicate_of: string;
  note?: string;
} | {
  kind: 'unmapped';
  point: LonLat;
  note?: string;
};

/** POST /v1/door-reports. */
export interface DoorReportReceipt {
  id: string;
  status: 'queued' | 'already_queued';
  kind: DoorReport['kind'];
  code: string | null;
  target: 'door' | 'building' | 'plot' | null;
  moved_m: number | null;
  received_at: string;
  next: 'review';
}

/** One day in usage(). */
export interface UsageDay {
  /** Ulaanbaatar day, YYYY-MM-DD. */
  day: string;
  calls: number;
  tiles: number;
  door_misses: number;
  keys: number;
}

/** One route of the org over the days asked (API 0.62.0). */
export interface UsageEndpoint {
  /** Method and route pattern, e.g. `GET /v1/door/:code`. Never a code, a name or a query. */
  endpoint: string;
  /** Requests to this route. The tiles route counts tiles. */
  calls: number;
  /** Of those, lookups that found no door. */
  misses: number;
}

/** Your daily caps; null is no cap. */
export interface UsageCaps {
  daily_quota: number | null;
  tile_daily_quota: number | null;
  door_miss_cap: number | null;
}

/** What a free org gets on the partner plan. Prices are not published yet. */
export interface UsageUpgrade {
  plan: 'partner';
  caps: UsageCaps;
  rate_limit: number;
  tile_rate_limit: number;
  added_scopes: string[];
}

/** GET /v1/usage: the calling key's organization. Caps are null when there is no cap. */
export interface UsageResponse {
  org: string;
  /** free: made by POST /v1/signup. custom: a partner org. */
  plan: 'free' | 'custom';
  /** The partner plan for a free org, null for a partner org. */
  upgrade: UsageUpgrade | null;
  timezone: string;
  /** Unix seconds of the next daily reset. */
  reset: number;
  key: {
    prefix: string | null;
    scopes: string[];
    rate_limit: number;
    tile_rate_limit: number;
  };
  caps: UsageCaps;
  /**
   * Features open to the org, A to Z, e.g. `cargo.eta` (API 0.51.0). Routes of any other feature answer 403
   * `feature_off`. Missing from an API before 0.51.0.
   */
  features?: string[];
  /** Newest first. */
  days: UsageDay[];
  /**
   * Every route the org called over the same days, most calls first, then A to Z (API 0.62.0). Missing from an API
   * before 0.62.0.
   */
  endpoints?: UsageEndpoint[];
}

/** How the route is travelled. */
export type RouteMode = 'driving' | 'walking' | 'cycling';

/** The door at one end of a route. Show `unofficial` as "(unofficial)" unless official_status is matched. */
export interface RouteDoor {
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  unofficial: boolean;
  code: string;
  natural_mn: string | null;
  natural_en: string | null;
  entrance: Entrance;
  confidence: number | null;
}

/**
 * The entrance at one end of a route, when that end was sent as an entrance id (GET /v1/address/{id}/entrances,
 * GET /v1/entrances/near). Show `unofficial` as "(unofficial)" unless official_status is matched; confidence 0 with
 * source generated was placed by rule (таамаг).
 */
export interface RouteEntrance {
  id: string;
  building: string;
  /** null: no door minted for this entrance yet. */
  code: string | null;
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  unofficial: boolean;
  kind: 'door' | 'gate' | 'vehicle_gate' | 'stairwell';
  letter: string | null;
  confidence: number | null;
  source: string;
}

/** The address unit (building or khashaa) a route end was given as, by its code. The route ends at its street side. */
export interface RouteUnit {
  code: string;
  kind: 'building' | 'khashaa';
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  unofficial: boolean;
  /** true: the end is the gate, or the outline of the building or khashaa at the street, within about 25 m of where the line meets the street. false: the outline is farther than that from the street, the walk is longer. */
  street_side: boolean;
}

/** One end of a route. */
export interface RouteEnd {
  /** What was asked for: the entrance when a code was given. */
  point: LonLat;
  /** Where the line starts or ends, on the road network. */
  snapped: LonLat;
  /** Straight metres from snapped to point. Walked, never drawn as road. */
  gap_m: number;
  door: RouteDoor | null;
  /** The entrance, when this end was sent as an entrance id; else null. Absent from older APIs. */
  entrance?: RouteEntrance | null;
  /** Present only when this end was sent as a unit code. */
  unit?: RouteUnit;
  /** The date of the door, unit or entrance behind this end, YYYY-MM-DD; null for a bare point. Absent before API 0.87.0. */
  source_date?: string | null;
}

/** The road closures a route avoided (GET /v1/closures, active on as_of). */
export interface RouteClosures {
  /** The route day, YYYY-MM-DD, Ulaanbaatar. */
  as_of: string;
  /** Closure ids, in id order. Each was kept out of the route, whether or not it came near. Show them with it. */
  avoided: string[];
}

/** Driving time with all partners' measured moving speed per road (API 0.48.0). */
export interface RouteMeasured {
  /** YYYY-MM: the latest month GET /v1/traces/roads publishes. */
  month: string;
  /** The hour band of departAt whose speeds were read (API 0.50.0); null without departAt: all hours. */
  band?: RoadBand | null;
  /** Meters of the line on roads with a published speed that month. 0: none, and duration_s is the free-flow one. */
  roads_m: number;
  /** Seconds with those roads at their measured moving speed, the rest at the router's. A month average of the band. */
  duration_s: number;
}

/** GET /v1/route. duration_s is free-flow; measured has partners' speeds. Avoids the road closures active today. */
export interface Route {
  mode: RouteMode;
  distance_m: number;
  /** Seconds at the router's free-flow speeds. */
  duration_s: number;
  /** Driving only; null when walking or cycling, or when speeds could not be read. Absent from an API before 0.48.0. */
  measured?: RouteMeasured | null;
  /** Driving: seconds with a typical load by road class, hour and weekday; null when walking, cycling or unreadable (API 0.97.19). */
  expected_duration_s?: number | null;
  /** Where that load comes from: model (a table), probes (partners' phones, not yet). */
  traffic_source?: 'model' | 'probes';
  /** The label to show beside expected_duration_s. */
  traffic_note?: string;
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  };
  /** One per hop, in order: from to via[0], ..., the last to to; one without via. Absent from an API before 0.67.0. */
  legs?: RouteLeg[];
  from: RouteEnd;
  /** The via ends in the order sent, snapped where the line passes them; empty without. Absent before API 0.67.0. */
  via?: RouteEnd[];
  to: RouteEnd;
  closures: RouteClosures;
  source: string;
}

/**
 * GET /v1/matrix (API 0.63.0): free-flow travel time and distance from every from to every to in one call. Cell
 * [i][j] goes from from[i] to to[j]; null where no path joins them. No measured speeds, no line.
 */
export interface Matrix {
  mode: RouteMode;
  /** Seconds at the router's free-flow speeds, as duration_s in a route. */
  durations_s: (number | null)[][];
  /** Metres from from[i].snapped to to[j].snapped; null where durations_s is null. */
  distances_m: (number | null)[][];
  /** The sources in the order asked, each as a route end. */
  from: RouteEnd[];
  /** The targets in the order asked, each as a route end. */
  to: RouteEnd[];
  closures: RouteClosures;
  source: string;
}

/** One isochrone contour: the area reached within `properties.contour` minutes, GeoJSON, every ring closed. */
export interface IsochroneFeature {
  type: 'Feature';
  /** contour in minutes; metric is always 'time', as Mapbox's contours_minutes. */
  properties: {
    contour: number;
    metric: 'time';
  };
  /** A Polygon (rings) or, where the area is in pieces, a MultiPolygon (polygons of rings), [lon, lat]. */
  geometry: {
    type: 'Polygon';
    coordinates: LonLat[][];
  } | {
    type: 'MultiPolygon';
    coordinates: LonLat[][][];
  };
}

/**
 * GET /v1/isochrone (API 0.64.0): GeoJSON, one feature per contour, the largest first, so drawn in order each smaller
 * one lies on top. Free-flow speeds, closures avoided. mode, from, closures and source sit beside the features.
 */
export interface Isochrone {
  type: 'FeatureCollection';
  features: IsochroneFeature[];
  mode: RouteMode;
  /** Where the areas are reached from, as a route end. */
  from: RouteEnd;
  closures: RouteClosures;
  source: string;
}

/** One unbroken piece of a matched trace (GET /v1/matching): the roads it went along, in its order. */
export interface Matching {
  distance_m: number;
  /** Seconds at the router's free-flow speed on each road, no turn costs; not the time the trace took. */
  duration_s: number;
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  };
}

/** Where one point sent lies on the matched line. */
export interface MatchTracepoint {
  /** The matching this point lies on, an index into matchings. */
  matchings_index: number;
  /** [lon, lat] on the matched line. */
  location: LonLat;
  /** Straight-line metres from the point sent to location. */
  gap_m: number;
}

/**
 * GET /v1/matching (API 0.65.0): a GPS trace on the road network. One matching per unbroken piece; one tracepoint per
 * point sent, in its order, null where no road was found for it or it is inside a restricted site.
 */
export interface Match {
  mode: RouteMode;
  matchings: Matching[];
  tracepoints: (MatchTracepoint | null)[];
  source: string;
}

/** One hop of an optimized trip, from one stop to the next in the order driven, or of a route with via (API 0.67.0). */
export interface TripLeg {
  /** Metres from one stop to the next along the road network. */
  distance_m: number;
  /** Seconds from one stop to the next at the router's free-flow speeds. */
  duration_s: number;
}

/** One turn-by-turn step of a route (API 0.74.0): what to do at location, then distance_m to the next step. */
export interface RouteStep {
  kind: 'depart' | 'left' | 'right' | 'slight_left' | 'slight_right' | 'sharp_left' | 'sharp_right' | 'uturn' | 'straight' | 'keep_left' | 'keep_right' | 'ramp_left' | 'ramp_right' | 'merge' | 'roundabout' | 'arrive' | 'arrive_left' | 'arrive_right';
  location: LonLat;
  distance_m: number;
  duration_s: number;
  /** The road taken here, as OpenStreetMap names it; null when unnamed. */
  street: string | null;
  /** Mongolian voice phrase ids in the order spoken (Navigator, VoicePlayer). */
  voice: string[];
  mn: string;
  en: string;
}

/** A route's leg: a trip leg with its steps (absent from an API before 0.74.0). */
export interface RouteLeg extends TripLeg {
  /** The floor of this leg: 0 on every road leg (absent from an API that does not send it yet). */
  floor?: number;
  steps?: RouteStep[];
}

/** The whole trip in the order driven, snapped stop to snapped stop. */
export interface Trip {
  distance_m: number;
  /** Seconds on the road at the router's free-flow speeds. No live traffic, no measured speeds, no time at the stops. */
  duration_s: number;
  geometry: {
    type: 'LineString';
    coordinates: LonLat[];
  };
  /** legs[k] goes from the stop with waypoint_index k to the one with k + 1; on a round trip the last goes back to the first. */
  legs: TripLeg[];
}

/** One stop as sent, a route end with its place in the trip. */
export interface TripWaypoint extends RouteEnd {
  /** Where this stop comes in the trip: 0 is the first, always the first sent. */
  waypoint_index: number;
}

/**
 * GET /v1/optimized-trips (API 0.66.0): the stops in the order with the least time on the road. trips holds one trip,
 * as Mapbox's answer does; waypoints are the stops in the order sent. Today's closures avoided.
 */
export interface OptimizedTrip {
  mode: RouteMode;
  /** true: the trip goes back to the first stop. false: it ends at the last stop sent. */
  roundtrip: boolean;
  trips: Trip[];
  waypoints: TripWaypoint[];
  closures: RouteClosures;
  source: string;
}

/** A tile source tilequery reads: the building and entrance layers of the map. */
export type TilequerySource = 'building' | 'entrance';

/** A feature's geometry type in its tile, as Mapbox's tilequery.geometry. */
export type TilequeryGeometry = 'polygon' | 'linestring' | 'point';

/** How a tilequery feature lies from the point sent. */
export interface TilequeryInfo {
  /** Metres from the point sent to the nearest part of the feature, to 0.1 m. 0: the point is inside it. */
  distance: number;
  geometry: TilequeryGeometry;
  /** The tile source and layer the feature is in. */
  layer: TilequerySource;
}

/** Second-source check of a building footprint. confidence is null when the run gave none: unknown. */
export interface FootprintCheck {
  verdict: 'confirmed' | 'new' | 'doubtful';
  source: string;
  confidence: number | null;
  checked_at: string;
}

/**
 * The fields of the feature's tile layer, and nothing more: a building has building fields, an entrance entrance
 * fields. null where the map does not know. No building codes.
 */
export interface TilequeryProperties {
  /** A building: whether its footprint is backed by a second source. null for other kinds and for a building not yet checked. */
  footprint_check?: FootprintCheck | null;
  /** The building or entrance id, as in its tile. Never an address. */
  id: string;
  /** Building only. */
  name_mn?: string | null;
  /** Building only. */
  name_en?: string | null;
  /** Building only. */
  type?: 'apartment' | 'ger_house' | 'commercial' | 'public' | 'industrial' | 'garage' | 'other' | null;
  /** Building only. */
  status?: 'planned' | 'under_construction' | 'built';
  /** Building only. Metres. */
  height_m?: number | null;
  /** Building only. */
  levels?: number | null;
  /** Building only: where height_m came from (osm:height, levels, default:*, estimated:*, unknown). */
  height_source?: string | null;
  /** Building only: 3 a tag or editor, 2 levels, 1 type default, 0 estimated; 0 and 1 are estimates. null: no height. */
  height_confidence?: number | null;
  /** Place only: the map category (bank, pharmacy, stop, religion, etc.). */
  cat?: string | null;
  /** Place only: the map rank (higher shows over lower in collisions). */
  rank?: number | null;
  /** Building only: official building number from the state register. null: no matched or partial official form. */
  building_number?: string | null;
  /** Building only: the building's name when it is a name, not a purpose. null: no name or no official form. */
  building_name?: string | null;
  /** Entrance only. */
  kind?: 'door' | 'gate' | 'vehicle_gate' | 'stairwell';
  /** Entrance only. */
  letter?: string | null;
  /** Entrance only: its building. null: none on record. */
  building_id?: string | null;
  /** 0 to 5 (PLAN.md 5). null: not rated yet. */
  confidence: number | null;
  source: string;
  tilequery: TilequeryInfo;
}

/** One building or entrance at or near the point: a Point, the part of it nearest the point sent. */
export interface TilequeryFeature {
  type: 'Feature';
  /** [lon, lat]; the point sent when it is inside. The 7th decimal is a mark for your organization. */
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: TilequeryProperties;
}

/**
 * GET /v1/tiles/{sources}/tilequery/{lon},{lat} (API 0.68.0), as Mapbox's Tilequery API: GeoJSON, nearest first, then
 * by layer and id. The same rows as the tiles: current, not demolished, nothing in a restricted site.
 */
export interface Tilequery {
  type: 'FeatureCollection';
  features: TilequeryFeature[];
}

/** Why a road is closed. */
export type ClosureKind = 'flood' | 'washout' | 'bridge_out' | 'snow' | 'mud' | 'works' | 'other';

/** One closure in GET /v1/closures. No partner name, no note. */
export interface ClosureItem {
  /** cl-000001. Never reused. */
  id: string;
  kind: ClosureKind;
  /** First closed day, YYYY-MM-DD Ulaanbaatar. */
  from: string;
  /** Last closed day, or null: until lifted. */
  to: string | null;
  /** One partner's word, not field checked. */
  confidence: number;
  perimeter_m: number;
  /** Ring vertices, the closing one included. */
  vertices: number;
  /** Closed ring, [lon, lat]. One of Valhalla's exclude_polygons. */
  ring: LonLat[];
}

/** GET /v1/closures: every closure not over today, the same shape as an offline pack's closures block. */
export interface ClosuresResponse {
  method: string;
  source: string;
  /** Today when it was fetched, YYYY-MM-DD Ulaanbaatar. */
  as_of: string;
  current: number;
  /** sha256 of the sorted ids joined by commas; null for none. An offline pack's closures_sha256. */
  ids_sha256: string | null;
  buffer_m: number;
  rule: string;
  active_when: string;
  /** Per route request, all rings together. */
  valhalla_limits: {
    perimeter_m: number;
    vertices: number;
  };
  items: ClosureItem[];
}

/** A closure's place: a Point on the stretch (not on a junction), or a LineString of 2 or more points, at most 3000 m. */
export type ClosureGeometry = {
  type: 'Point';
  coordinates: LonLat;
} | {
  type: 'LineString';
  coordinates: LonLat[];
};

/** POST /v1/closures. The sender is the key's organization. */
export interface ClosureReport {
  geometry: ClosureGeometry;
  kind: ClosureKind;
  /** First closed day, YYYY-MM-DD Ulaanbaatar. Default today. */
  from?: string;
  /** Last closed day. Absent or null: until lifted. */
  to?: string | null;
  /** Your words for your own records, at most 200 characters; never served to others. No names, no phone numbers. */
  note?: string;
}

/** One of your organization's closure reports, as stored (POST /v1/closures, GET /v1/closures/{id}, lift). */
export interface Closure {
  type: 'Feature';
  /** cl-000001. Never reused. */
  id: string;
  geometry: ClosureGeometry;
  properties: {
    kind: ClosureKind;
    from: string;
    to: string | null;
    /** Open again from this day, YYYY-MM-DD; set once. null: not lifted. */
    lifted: string | null;
    /** As sent; '' for none. */
    note: string;
    confidence: number;
    /** UTC ISO 8601. */
    reported_at: string;
  };
}

/** POST /v1/telemetry/search. */
export interface SearchTelemetryReceipt {
  stored: number;
  /** Text that was only a phone or an e-mail address, or a pick not in the index. */
  dropped: number;
}

/** POST /v1/traces. */
export interface TraceReceipt {
  /** Keep it to fetch or delete the trip. null: nothing was stored (every point was inside a restricted site). */
  id: string | null;
  points: number;
  /** Points left out: inside a restricted site. */
  restricted: number;
  /** Your organization sent this trip before; id is the first upload. */
  duplicate: boolean;
  /** Raw points are erased after this day, YYYY-MM-DD. */
  purge_after: string | null;
}

/** GET /v1/traces/{id}: your trace as stored, GeoJSON. geometry is null once deleted or past purge_after. */
export interface Trace {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'LineString';
    coordinates: [number, number, number][];
  } | null;
  properties: {
    received_day: string;
    purge_after: string;
    points: number;
    restricted: number;
    deleted_at: string | null;
    deleted_reason: 'partner' | 'retention' | null;
  };
}

/** DELETE /v1/traces/{id}. deleted is true also when the points were gone before. */
export interface TraceDeleted {
  id: string;
  deleted: boolean;
}

/** west, south, east, north in degrees. */
export type BBox = [number, number, number, number];

/** One 500 m cell of the fixed grid in a month. id is "x/y", the same cell every month. */
export interface TraceCell {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'Polygon';
    coordinates: [number, number][][];
  };
  /** Distinct trips through the cell that month. */
  properties: {
    trips: number;
  };
}

/** GET /v1/traces/cells: all partners together, only cells that meet `rule`. No cell means too few, not zero. */
export interface TraceCells {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  rule: Record<string, unknown>;
  features: TraceCell[];
}

/** One 500 m cell of the trip cells grid with all partners' moving speed that month. id is "x/y", as TraceCell. */
export interface TraceSpeed {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'Polygon';
    coordinates: [number, number][][];
  };
  /** Counted meters over counted seconds of all partners' trips in the cell, to 0.1. No trip count. */
  properties: {
    km_h: number;
  };
}

/** GET /v1/traces/speeds: all partners together, only cells that meet `rule`. No cell means too few trips, not slow. */
export interface TraceSpeeds {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** The group rule each cell passed, and how a segment and km_h are measured, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Cells north to south, then west to east. */
  features: TraceSpeed[];
}

/** A road's line: dock's current road with that OSM id, a LineString, or a MultiLineString when it is in pieces. */
export type TraceRoadGeometry = {
  type: 'LineString';
  coordinates: LonLat[];
} | {
  type: 'MultiLineString';
  coordinates: LonLat[][];
};

/** One OSM way with all partners' moving speed that month. id is the OSM way id as text: it can pass 2^53 one day. */
export interface TraceRoad {
  type: 'Feature';
  id: string;
  geometry: TraceRoadGeometry;
  properties: {
    /** Counted meters over counted seconds of all partners' trips matched to the way, to 0.1. No trip count. */
    km_h: number;
    /** The road's names and OSM highway class, null when unknown. */
    name_mn: string | null;
    name_en: string | null;
    highway: string | null;
  };
}

/**
 * Hour band of GET /v1/traces/roads (API 0.49.0), by the Ulaanbaatar time of a trip's first stored point: am_peak Monday
 * to Friday 07:00 to 09:59, pm_peak Monday to Friday 17:00 to 19:59, off_peak every other hour, Saturday and Sunday whole.
 */
export type RoadBand = 'am_peak' | 'pm_peak' | 'off_peak';

/** GET /v1/traces/roads, what traceRoads() returns. */
export interface TraceRoads {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** The group rule each road passed, and how a trip is matched and km_h measured, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Roads by OSM way id, smallest first. */
  features: TraceRoad[];
}

/** POST /v1/traffic/probes, what stopProbes() returns (API 0.82.0). */
export interface ProbeReceipt {
  /** Points in the batch. */
  points: number;
  /** Points left out: inside a restricted site. */
  restricted: number;
  /** Road and 15-minute window sums taken. */
  roads: number;
  /** Road and window sums left out: their window had closed, 5 minutes after it ended. */
  late: number;
}

/** A road on GET /v1/traffic. id is the OSM way id as text. */
export interface TrafficRoad {
  type: 'Feature';
  id: string;
  geometry: TraceRoadGeometry;
  properties: {
    /** Moving meters over moving seconds of the vehicles on the way in the window, to 0.1. */
    km_h: number;
    /** Vehicles counted, 5 or more. null when the answer is typical. */
    vehicles: number | null;
    /** The road's names and OSM highway class, null when unknown. */
    name_mn: string | null;
    name_en: string | null;
    highway: string | null;
  };
}

/** A road on GET /v1/traffic/typical. id is the OSM way id as text. */
export interface TypicalTrafficRoad {
  type: 'Feature';
  id: string;
  geometry: TraceRoadGeometry;
  properties: {
    /** The usual moving speed on the way in this hour band, to 0.1. */
    km_h: number;
    /** The road's names and OSM highway class, null when unknown. */
    name_mn: string | null;
    name_en: string | null;
    highway: string | null;
  };
}

/** GET /v1/traffic/typical, what trafficTypical() returns (API 0.96.0): the usual speeds of this hour band, never live. */
export interface TypicalTraffic {
  type: 'FeatureCollection';
  kind: 'typical';
  /** The label to show ("ердийн"), the hour band of this moment (Ulaanbaatar clock), and the closed month the speeds come from; month is null when no road has a speed. */
  typical: {
    label: string;
    band: string;
    hours: string;
    month: string | null;
  };
  /** The rule each road passed, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Where the speeds come from: partner traces, all partners together. */
  source: string;
  /** The last day of the month the speeds come from; null when no road has a speed. */
  source_date: string | null;
  /** Roads by OSM way id. Empty is valid: no traces, no speed. */
  features: TypicalTrafficRoad[];
}

/** GET /v1/traffic, what traffic() returns (API 0.82.0): the last 15-minute window that has ended. */
export interface Traffic {
  type: 'FeatureCollection';
  /** ISO times, UTC, on the quarter hour. null when the answer is typical. */
  window: {
    from: string;
    to: string;
  } | null;
  /** The rule each road passed, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Roads by OSM way id, smallest first. */
  features: TrafficRoad[];
  /** live: fleet probes of the last ended window. typical: no live road passed the rule, the speeds are the usual ones for this hour band. */
  kind: 'live' | 'typical';
  /** null when live. When typical: the label to show ("ердийн"), the hour band of this moment and the closed month the speeds come from. */
  typical: {
    label: string;
    band: string;
    hours: string;
    month: string;
  } | null;
  /** Where the speeds come from: partner fleet traces. */
  source: string;
  /** live: the date the window ended. typical: the last day of the month the speeds come from. */
  source_date: string;
}

/** An organizer's event, what createEvent() and event() return (API 0.83.0). */
export interface DockEvent {
  id: string;
  name: string;
  /** [west, south, east, north]: points outside are not stored. */
  bbox: BBox;
  /** ISO times, UTC. */
  starts_at: string;
  ends_at: string;
  /** When every point is erased: 15 minutes after ends_at. */
  closes_at: string;
  /** true once the points are erased; the heat cells stay. */
  closed: boolean;
}

/** What createEvent() takes. */
export interface EventInput {
  /** The event's name, 1 to 120 characters. Never a person's. */
  name: string;
  /** Inside Mongolia, sides at most 1 degree. */
  bbox: BBox;
  /** ISO time with a zone, or a Date. */
  startsAt: string | Date;
  /** After startsAt and after now, at most 14 days after startsAt. */
  endsAt: string | Date;
}

/** POST /v1/events/{id}/positions, what stopEventPositions() returns. */
export interface EventPositionsReceipt {
  /** Points in the batch. */
  points: number;
  /** Points stored until the event closes. */
  kept: number;
  /** Points left out: outside the event's area or time. */
  outside: number;
  /** Points left out: inside a restricted site. */
  restricted: number;
}

/** A live participant's last point, to 4 decimals. Nothing about the participant. */
export interface EventLivePoint {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: LonLat;
  };
  properties: Record<string, never>;
}

/** GET /v1/events/{id}/live, what eventLive() returns. */
export interface EventLive {
  type: 'FeatureCollection';
  /** The answer's time; points are from the 2 minutes before it. */
  at: string;
  /** Participants shown; 0 when fewer than 5 are live, then features is empty. */
  participants: number;
  /** The rule the answer follows, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Sorted by place. */
  features: EventLivePoint[];
}

/** A heat cell: a 150-unit EPSG:3857 square, about 100 m at Ulaanbaatar. */
export interface EventHeatCell {
  type: 'Feature';
  geometry: {
    type: 'Polygon';
    coordinates: LonLat[][];
  };
  /** Distinct participants with a point in the cell, 5 or more. */
  properties: {
    participants: number;
  };
}

/** GET /v1/events/{id}/heat, what eventHeat() returns. */
export interface EventHeat {
  type: 'FeatureCollection';
  /** The 15-minute window; null for the whole event. */
  window: {
    from: string;
    to: string;
  } | null;
  /** true once the event is closed: the cells are final. */
  closed: boolean;
  /** The rule each cell passed, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** North to south, west to east. */
  features: EventHeatCell[];
}

/** GET /v1/traces/cells/own: your organization's trips only, every cell with one of them. */
export interface OwnTraceCells extends TraceCells {
  /** false while trips received now still count; true from 14 days after the month ends. */
  closed: boolean;
}

/** Trips per whole arrival minute. */
export interface ArrivalMinute {
  /** Whole minutes; 60 is 60 or more. */
  minutes: number;
  trips: number;
}

/** A 500 m cell of the trip cells grid where your trips ended. id is "x/y", the same cell every month. */
export interface ArrivalCell {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'Polygon';
    coordinates: [number, number][][];
  };
  properties: {
    /** Your trips that ended in the cell that month. */
    trips: number;
    /** Of them, arrivals of 5 minutes or more. */
    slow: number;
    /** Median arrival minutes in the cell, nearest rank. */
    median: number;
  };
}

/** GET /v1/traces/arrivals/own: how long your trips spend in the last 200 m. Your organization's trips only. */
export interface OwnArrivals {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** false while trips received now still count; true from 14 days after the month ends. */
  closed: boolean;
  /** How arrival minutes are measured, keys as the API writes them (radius_m, max_minutes, slow_minutes, ...). */
  rule: Record<string, unknown>;
  /** Your trips with an arrival that month, every cell. */
  trips: number;
  /** Of them, arrivals of 5 minutes or more. */
  slow: number;
  /** Nearest rank. null with no trips. */
  median: number | null;
  p75: number | null;
  p90: number | null;
  /** Trips per arrival minute, fewest minutes first; minutes with none left out. */
  histogram: ArrivalMinute[];
  /** Cells your trips ended in; features lists up to 100 of them. */
  cells: number;
  /** By slow trips, then trips, most first. */
  features: ArrivalCell[];
}

/** Trips that started in one Ulaanbaatar weekday and hour: yours in OwnHours, all partners' in AllHours. */
export interface HourSlot {
  /** ISO weekday in Ulaanbaatar, 1 Monday to 7 Sunday. */
  weekday: number;
  /** Hour in Ulaanbaatar, 0 to 23. */
  hour: number;
  trips: number;
}

/** GET /v1/traces/hours/own: when your trips start, by weekday and hour. Your organization's trips only. */
export interface OwnHours {
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** false while trips received now still count; true from 14 days after the month ends. */
  closed: boolean;
  /** How the weekday and hour are read, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Your trips that month, each once, at the weekday and hour of its first stored point. */
  trips: number;
  /** 7 counts, Monday first. */
  by_weekday: number[];
  /** 24 counts, hour 0 first. */
  by_hour: number[];
  /** Weekday and hour with trips, weekday then hour; slots with none left out. */
  slots: HourSlot[];
}

/** GET /v1/traces/hours: when trips start, by weekday and hour, all partners together. A published month only. */
export interface AllHours {
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** The group rule each slot passed, and how the weekday and hour are read, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Weekday and hour that passed the rule, weekday then hour. A slot left out had too few trips, not zero. No total. */
  slots: HourSlot[];
}

/** Trips that ended in one khoroo in one Ulaanbaatar hour, all partners together. */
export interface KhorooHour {
  /** Hour in Ulaanbaatar of the trip end, 0 to 23. */
  hour: number;
  trips: number;
}

/** A current khoroo polygon with the hours of arrival that passed the rule. id is the polygon's, never an address. */
export interface TraceKhoroo {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'MultiPolygon';
    coordinates: [number, number][][][];
  };
  properties: {
    district_mn: string;
    district_en: string | null;
    /** Khoroo number in its district. */
    khoroo: number;
    /** Hours that passed the rule, from 0. An hour left out had too few trips, not zero. No khoroo total. */
    hours: KhorooHour[];
  };
}

/** GET /v1/traces/khoroos: where and when trips end, per khoroo and hour, all partners together. A published month only. */
export interface TraceKhoroos {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** The group rule each khoroo and hour passed, and how they are read, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Khoroos with at least one hour shown, by district then number. */
  features: TraceKhoroo[];
}

/** A current khoroo polygon with your trips that ended in it. id is the polygon's, never an address. */
export interface OwnKhoroo {
  type: 'Feature';
  id: string;
  geometry: {
    type: 'MultiPolygon';
    coordinates: [number, number][][][];
  };
  properties: {
    district_mn: string;
    district_en: string | null;
    /** Khoroo number in its district. */
    khoroo: number;
    /** Your trips that ended in this khoroo that month. */
    trips: number;
    /** Your trips per Ulaanbaatar hour of arrival, from 0. Hours with none left out. */
    hours: KhorooHour[];
  };
}

/** GET /v1/traces/khoroos/own: where and when your trips end, per khoroo and hour. Your organization only, no group rule. */
export interface OwnKhoroos {
  type: 'FeatureCollection';
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** false while trips received now still count; true from 14 days after the month ends. */
  closed: boolean;
  /** How the khoroo and hour are read, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Your trips that month. */
  trips: number;
  /** Your trips that month ending in no current khoroo polygon. trips is no_khoroo plus every khoroo's trips. */
  no_khoroo: number;
  /** Khoroos with at least one of your trips, by district then number. */
  features: OwnKhoroo[];
}

/** The building type at a trip end. unknown: a building with no type yet; none: no building within 30 m. */
export type BuildingKind = 'apartment' | 'ger_house' | 'commercial' | 'public' | 'industrial' | 'garage' | 'other' | 'unknown' | 'none';

/** Your trips that month ending at one kind of building. */
export interface OwnKind {
  kind: BuildingKind;
  trips: number;
}

/** GET /v1/traces/kinds/own: what kind of building your trips end at, per month. Your organization only, no group rule. */
export interface OwnKinds {
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** false while trips received now still count; true from 14 days after the month ends. */
  closed: boolean;
  /** How the building and its kind are read, keys as the API writes them. */
  rule: Record<string, unknown>;
  /** Your trips that month, the sum of every kind's trips. */
  trips: number;
  /** Kinds with at least one of your trips, most trips first, then by name. */
  kinds: OwnKind[];
}

/** Ids of what a door stands in. Ids only, never shown to people as an address. null when not known. */
export interface Parents {
  /** The door of a business. null for a door. */
  door: string | null;
  building: string | null;
  /** Khashaa (plot). */
  plot: string | null;
  area: string | null;
  street: string | null;
  /** null when no khoroo polygon covers the door. Never guessed. */
  khoroo: string | null;
  district: string | null;
}

/** GET /v1/address/{id}: one place by the id geocode, reverse or changes gave. */
export interface Address extends Place {
  /** A landmark, business or street: the district its point stands in, in Mongolian. null when no polygon covers it, and for any other kind. */
  district_mn?: string | null;
  /** A building: whether its footprint is backed by a second source. null for other kinds and for a building not yet checked. */
  footprint_check?: FootprintCheck | null;
  /** A building: what it is (apartment, ger_house, commercial, public, industrial, garage, other). null when it has no type yet. Absent for other kinds. */
  building_type?: string | null;
  /** A building: true when its whole name is only a kind word. null when not checked. Absent for other kinds. */
  name_is_kind_word?: boolean | null;
  /** A landmark or building: one openly licensed photo (CC BY, CC BY-SA, CC0 or public domain) within 30 m. Show credit and license beside it. null when there is none. */
  photo?: {
    url: string;
    credit: string;
    license: string;
    source: string;
  } | null;
  /** Set only when photo is null: where to point the map from above (zoom 19 for a building, door or business, 17 otherwise). null when there is a photo or the site is restricted. */
  map_from_above?: {
    lon: number;
    lat: number;
    zoom: number;
  } | null;
  /** Doors and businesses; a khoroo carries its district only; null for every other kind. */
  parents: Parents | null;
  /** A bus stop: the city bus lines that call at it ("Ч:41"), as nearby() gives them. null for every other place. */
  lines: string[] | null;
  /** A business or landmark: its map category, as nearby() gives it. null for any other kind. */
  category: string | null;
  /** A company from a building's directory (API 0.90.0): its floor from the legal address in the state register. null when not said, and for every other place. */
  floor?: number | null;
  /** A company from a building's directory (API 0.90.0): its flat or office number (N тоот). null when not said. */
  flat?: string | null;
  /** A district, khoroo or area (next API patch): its boundary as a GeoJSON Polygon or MultiPolygon, about 5 m simplified. null for every other kind and where no polygon is held. */
  outline?: {
    type: 'Polygon' | 'MultiPolygon';
    coordinates: unknown;
  } | null;
  /** A district, khoroo or area: current buildings and doors inside its boundary (0 is a real zero), with source and as_of. null for every other kind and where no polygon is held: unknown. */
  counts?: {
    buildings?: number;
    doors?: number;
    source?: string;
    as_of?: string;
    [key: string]: unknown;
  } | null;
}

/**
 * One entrance of a building, in entrances(). With a door: its code and the door's official fields. Without one:
 * code null and the building's official form with this entrance's number.
 */
export interface BuildingEntrance {
  official_address: OfficialAddress | null;
  /** The level: matched бүртгэлтэй, partial хэсэгчилсэн, none албан бус. */
  official_status: OfficialStatus;
  /** true unless official_status is matched: show "unofficial". */
  unofficial: boolean;
  official_source: string | null;
  official_date: string | null;
  /** The date of this entrance's current version, YYYY-MM-DD. null: unknown. */
  source_date: string | null;
  /** The door code. null: no door minted for this entrance yet. */
  code: string | null;
  /** The entrance id. Never show it to people as an address. */
  id: string;
  kind: Entrance['kind'];
  /** Entrance or gate number as people say it. null: not known. */
  letter: string | null;
  floors_served: number[] | null;
  point: LonLat;
  /** PLAN 5 level of the point. 0 with source generated: placed by rule from the footprint, show it as таамаг. */
  confidence: number | null;
  source: string;
}

/** GET /v1/address/{id}/entrances: a building's official form and its entrances. */
export interface BuildingEntrances {
  /** The building id, as sent. */
  building: string;
  official_address: OfficialAddress | null;
  official_status: OfficialStatus;
  unofficial: boolean;
  official_source: string | null;
  official_date: string | null;
  /** Numbered entrances first, in number order, then the rest. Empty when none is mapped or generated. */
  entrances: BuildingEntrance[];
  /** false until the building's entrance method is approved; entrances is then empty. */
  entrances_approved: boolean;
  /** Present when entrances_approved is false. */
  note?: string;
}

/** One organization in a building's directory. Only organizations matched to an official registration are listed. */
export interface DirectoryEntry {
  name: string;
  /** Floor (давхар). null when the register does not say. */
  floor: number | null;
  /** Unit (тоот). null when unknown. */
  unit: string | null;
  /** The flat or office number, the same value as `unit`, named as geocode and nearby name it. null when unknown. Added in API 0.96.5. */
  flat: string | null;
  category: string | null;
  source: string;
  confidence: number | null;
  /** Date this version of the entry was recorded. */
  source_date: string;
}

/** GET /v1/address/{id}/directory: "Энд байгаа байгууллагууд" for an office building or mall card. */
export interface BuildingDirectory {
  /** The building id, as sent or the one the code names. null for a хашаа code. */
  building: string | null;
  /** The building's code. null until one is minted. */
  code: string | null;
  /** By floor (unknown floor last), then unit, then name. Empty when none is known. */
  entries: DirectoryEntry[];
}

/** One entrance in entrancesNear(): as a building's list shows it, plus its building and distance. */
export interface NearEntrance extends BuildingEntrance {
  /** The building id: entrances(building) lists all its entrances. */
  building: string;
  /** Metres from the query point. */
  distance_m: number;
}

/** GET /v1/entrances/near. */
export interface NearEntrances {
  /** The query point, as sent. */
  point: LonLat;
  /** Nearest first, within radius. Empty when no entrance is that close. */
  results: NearEntrance[];
}

/** A place in changes(): current, retired (gone, id never reused) or hidden (gone, may come back). */
export type ChangeState = 'current' | 'retired' | 'hidden';

/** One place changed, in changes(). */
export interface Change {
  /** The id place(id) takes. */
  id: string;
  kind: PlaceKind;
  /** current: fetch it again with place(id). retired: drop it, the id is never reused. hidden: drop it, it may come back. */
  state: ChangeState;
  /** Last change, UTC, microseconds. */
  changed_at: string;
}

/** GET /v1/changes. Window [since, until). */
export interface ChangesResponse {
  since: string;
  /** The last index refresh. When next is null, poll again with since = until. */
  until: string;
  /** Oldest first. One row per place, its last change. */
  changes: Change[];
  /** More in this window: pass as cursor. null: window done. */
  next: string | null;
}

/** What the driver found: the door is right, elsewhere, or gone. */
export type ConfirmResult = 'confirmed' | 'wrong_location' | 'missing';

/**
 * POST /v1/confirm body. confirmed and wrong_location need `tap`, the point the driver tapped on the map: the
 * placed point. missing has no tap.
 */
export type Confirmation = ConfirmationBase & ConfirmTarget & ({
  result: 'confirmed' | 'wrong_location';
  tap: LonLat;
} | {
  result: 'missing';
  tap?: never;
});

/** POST /v1/confirm. Map data does not change at once. */
export interface ConfirmReceipt {
  /** Same id for a repeat. */
  id: string;
  /** already_recorded: the same driver (or your org, with no driver) said the same in the last 24 h. */
  status: 'recorded' | 'already_recorded';
  result: ConfirmResult;
  /** unit: the code named a building or khashaa (an arrival). */
  target: 'door' | 'entrance' | 'unit';
  code: string | null;
  /** The entrance confirmed, also for a door code. null for a unit code. */
  entrance: string | null;
  /** Metres from the tap to the entrance on the map. null for missing. */
  moved_m: number | null;
  /** Metres from the tap (missing: the entrance) to the phone GPS. */
  gps_m: number;
  observed_at: string | null;
  received_at: string;
  /** confidence: counts toward the entrance confidence (PLAN.md 5). review: a person checks it first. arrival: recorded as an arrival at a building or khashaa, no entrance raised. */
  next: 'confidence' | 'review' | 'arrival';
}

/** The answer to sendConfirmPhoto (API 0.71.0). */
export interface ConfirmPhotoReceipt {
  /** The confirmation the photo belongs to. */
  id: string;
  /** queued: stored with the confirmation. unchanged: this photo already was. */
  status: 'queued' | 'unchanged';
  /** SHA-256 of the photo as stored, after its metadata was removed: not the hash of the file you sent. */
  sha256: string;
  received_at: string;
  /** A person reviews it once the entrance has a door. It is never shown on a public page. */
  next: 'review';
}

/** Confirmations and door reports by result. duplicate and unmapped come from door reports. */
export interface ConfirmCounts {
  confirmed: number;
  wrong_location: number;
  missing: number;
  duplicate: number;
  /** A real door with no code yet. */
  unmapped: number;
}

/** One Ulaanbaatar day the drivers were on site (observed_at, else received). */
export interface ConfirmDay extends ConfirmCounts {
  /** YYYY-MM-DD. */
  day: string;
  /** Different drivers that day, by your driver ids. Rows with no driver id are not counted here. */
  drivers: number;
}

/** A failure point: a door or entrance your drivers said is wrong, missing or a duplicate. */
export interface ConfirmFlagged {
  /** null when your drivers named the entrance, not a code. */
  code: string | null;
  /** The entrance id, as in the entrance tile layer. */
  entrance: string;
  /** true when the door or entrance has since been retired. */
  retired: boolean;
  wrong_location: number;
  missing: number;
  duplicate: number;
  /** The newest of them, on site. */
  last_at: string;
}

/** GET /v1/confirm/own: your organization's rows only. */
export interface OwnConfirmations {
  /** YYYY-MM, Ulaanbaatar. */
  month: string;
  /** false while confirmations queued offline can still join; true from 8 days after the month ends. */
  closed: boolean;
  totals: ConfirmCounts;
  /** Different drivers in the month, by your driver ids. Never the ids. */
  drivers: number;
  /** Days with at least one row, oldest first. */
  days: ConfirmDay[];
  /** Most first, at most 100. None inside a restricted site. */
  flagged: ConfirmFlagged[];
}

/** A door name your organization holds (API 0.52.0, feature door_names). Never a door code, never a phone. */
export interface DoorName {
  /** As registered, spaces collapsed. */
  name: string;
  /** The door it names. */
  code: string;
  registered_at: string;
  /** After this the name finds nothing and any organization may take it. */
  expires_at: string;
}

/** POST /v1/door-names. registered: yours from now. moved: your name now names this door. unchanged: it already did. */
export interface DoorNameReceipt extends DoorName {
  status: 'registered' | 'moved' | 'unchanged';
}

/** One of your door names, in doorNames(). */
export interface OwnDoorName extends DoorName {
  /** true after expires_at: it finds nothing, and stays listed until another organization takes it. */
  expired: boolean;
  /** When the transfer code you made stops working (API 0.53.0); null when none is waiting. */
  transfer_until: string | null;
}

/** GET /v1/door-names: your organization's names, A to Z. */
export interface OwnDoorNames {
  /** Unexpired names one organization may hold at once. */
  limit: number;
  names: OwnDoorName[];
}

/** GET /v1/door-names/{name}: the door as door() answers it, official address first. The holder is never shown. */
export interface NamedDoor {
  name: string;
  expires_at: string;
  door: Door;
}

/** POST /v1/door-names/{name}/transfer-code (API 0.53.0). The code is shown once, never again. */
export interface DoorNameTransfer {
  name: string;
  /** dnt_ and 24 characters. Anyone holding it can claim the name: give it only to the organization taking it. */
  transfer_code: string;
  /** Refused after this: 7 days, or the name's expires_at if sooner. */
  transfer_until: string;
}

/** DELETE /v1/door-names/{name}/transfer-code. cancelled: a waiting code was cleared; false: none was waiting. */
export interface DoorNameTransferCancel {
  name: string;
  cancelled: boolean;
}

/** POST /v1/door-names/{name}/claim: the name is yours from now; the door and expires_at are as they were. */
export interface DoorNameClaim extends DoorName {
  status: 'transferred';
}

/**
 * POST /v1/door-names/{name}/notes (API 0.58.0, feature hotel.notes): how guests find your entrance. 1 to 500
 * characters each, no phone number, no e-mail. null or absent: none in that language; give at least one.
 */
export interface DoorNotesBody {
  instructions_mn?: string | null;
  instructions_en?: string | null;
}

/** POST /v1/door-names/{name}/notes. queued: a new note waits for review. updated: your waiting note has this text now. unchanged: it had. */
export interface DoorNoteReceipt {
  /** The waiting note; writing again keeps it. */
  id: string;
  status: 'queued' | 'updated' | 'unchanged';
  name: string;
  /** The door the name points at, which the note is for. */
  code: string;
  instructions_mn: string | null;
  instructions_en: string | null;
  /** When the waiting text was last written. */
  received_at: string;
  /** A person reviews it. Until then door() serves the notes the map holds, not this one. */
  next: 'review';
}

/** One of your notes in doorNotes(). */
export interface DoorNoteState {
  id: string;
  /** waiting: not reviewed, guests do not see it. accepted: door() serves the languages it gave. refused: not used. */
  state: 'waiting' | 'accepted' | 'refused';
  instructions_mn: string | null;
  instructions_en: string | null;
  /** When this text was last written. */
  received_at: string;
  /** When a person accepted or refused it; null while it waits. */
  reviewed_at: string | null;
}

/** GET /v1/door-names/{name}/notes (API 0.59.0): your org's notes for the door, never another org's. */
export interface DoorNotes {
  name: string;
  /** The door the name points at now. */
  code: string;
  /** The waiting one first, then the newest review, at most 20. Empty: you wrote none. */
  notes: DoorNoteState[];
}

/** POST /v1/door-names/{name}/photo (API 0.60.0). queued: a new photo waits for review. updated: your waiting photo is now this one. unchanged: it was. */
export interface DoorPhotoReceipt {
  /** The waiting photo; sending again keeps it. */
  id: string;
  status: 'queued' | 'updated' | 'unchanged';
  name: string;
  /** The door the name points at, which the photo is for. */
  code: string;
  /** SHA-256 of the photo as stored, after its metadata was removed: not the hash of the file you sent. */
  sha256: string;
  /** Size as stored, in bytes. */
  bytes: number;
  /** Pixels as stored, before the EXIF orientation turns it. */
  width: number;
  height: number;
  /** When the waiting photo was last sent. */
  received_at: string;
  /** A person reviews the photo. Once accepted, doorPhoto() serves it. */
  next: 'review';
}

/** doorPhoto() (API 0.61.0): the accepted gate photo, for your app only. */
export interface DoorPhoto {
  /** SHA-256 of the photo served, as sendDoorPhoto() answered it. Pass it back to skip the bytes when unchanged. */
  sha256: string;
  /** The JPEG, metadata removed, orientation kept. null: the photo with the sha256 you passed is still the one. */
  jpeg: Uint8Array | null;
}