Skip to content

Android SDK лавлах ​

mn.dock:dock-android Kotlin сангийн нийтийн зарлал бүр, эх кодонд бичигдсэнээр, KDoc тайлбартайгаа: Kotlin хөрвүүлэгчийн өөрийн задлагчаар уншсан, эх файл бүрт нэг блок, Client.kt эхэнд. Биеийг болон annotation-ийг (@Serializable) орхисон. Тайлбар нь кодонд бичигдсэн тул англиар. sdk/android/src/main/kotlin/mn/dock-оос scripts/android-reference.ts бичнэ; энэ хуудас кодоос хоцорсон, төрөл, функц, класс, interface эсвэл object-ийн гишүүн тайлбаргүй, эсвэл зарлал төрлөө бичээгүй бол make test-docs унана. Дуудлага бүрийг жишээтэй нь: Android SDK.

Client.kt ​

kotlin
/** Version of this SDK. Semantic versioning (PLAN.md 21). */
const val DOCK_SDK_VERSION: String

/** The dock API, used when Client(key) gets no baseUrl. */
const val DEFAULT_BASE_URL: String

/** Ends per side in one matrix call, as the API's MATRIX_MAX (api/src/modules/route/route-input.ts). */
const val MATRIX_MAX: Int

/** Contours in one isochrone call, as the API's CONTOURS_MAX (api/src/modules/route/route-input.ts). */
const val ISOCHRONE_CONTOURS_MAX: Int

/** The longest contour in minutes, as the API's CONTOUR_MINUTES_MAX. */
const val ISOCHRONE_MINUTES_MAX: Int

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

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

/** The widest tilequery radius in metres, as the API's TILEQUERY_RADIUS_MAX_M (api/src/modules/tiles/tilequery.ts). */
const val TILEQUERY_RADIUS_MAX_M: Double

/** Features in one tilequery answer, as the API's TILEQUERY_LIMIT_MAX; 5 when no limit is sent. */
const val TILEQUERY_LIMIT_MAX: Int

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

/** Same shape as the API's key check (api/src/modules/auth/api-key.ts): dk_ and 32 of 0-9, a-z without i, l, o, u. */
fun isValidKey(key: String): Boolean

/** One request. toString shows method and URL only: the key lives in [headers]. */
class HttpRequest(val method: String, val url: String, val headers: Map<String, String>, val body: ByteArray?) {
    override fun toString(): String
}

/** One answer. [headers] look up case-insensitively. */
class HttpResponse(val status: Int, headers: Map<String, String>, val body: ByteArray) {
    /** The response headers. Names look up case-insensitively. */
    val headers: Map<String, String>
}

/** Sends one request. HttpURLConnection by default; tests and apps with their own networking (OkHttp) pass another.
 * Throw IOException when no answer came back. */
fun interface DockTransport {
    /** The answer to [request], whatever its status. Throws IOException when no answer came back. */
    suspend fun send(request: HttpRequest): HttpResponse
}

/** HttpURLConnection on Dispatchers.IO. On Android, plain http (the local stack) needs a network security config. */
class UrlConnectionTransport(
    connectTimeoutMs: Int = 15_000,
    readTimeoutMs: Int = 30_000,
) : DockTransport {
    override suspend fun send(request: HttpRequest): HttpResponse
}

/** 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. */
class DockError(
    /** HTTP status, 0 when no response came back. */
    val status: Int,
    val code: String,
    override val message: String,
    /** Seconds, from Retry-After on 429 and 503. null otherwise. */
    val retryAfter: Double? = null,
    cause: Throwable? = null,
) : Exception(message, cause) {
    override fun toString(): String
}

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

/** Notice from response headers, null when the route is not deprecated. */
fun deprecationNotice(route: String, headers: Map<String, String>): DeprecationNotice?

/**
 * The dock API v1. Calls send the key in the Authorization header. Map requests to this API (TileJSON, tiles)
 * get ?access_token= through [authorizedUrl], for MapLibre Native. The key never shows in toString or logs.
 *
 * @param key dk_ and 32 characters. Lives here only.
 * @param baseUrl default https://geo.dock.mn.
 * @param onDeprecation called once per route when an answer says the route is deprecated. Default: one line to
 *   System.out per route. Pass `{}` to stay quiet.
 * @param telemetry 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.
 * @param placeCache 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 `PlaceCacheOptions(store = ...)` to keep them across
 *   launches, null for none. Throws IllegalArgumentException when max is not 1 to 1000.
 */
class Client(
    key: String,
    baseUrl: String = DEFAULT_BASE_URL,
    transport: DockTransport = UrlConnectionTransport(),
    onDeprecation: ((DeprecationNotice) -> Unit)? = null,
    telemetry: Boolean = false,
    placeCache: PlaceCacheOptions? = PlaceCacheOptions(),
) {
    /** Where calls go: https://geo.dock.mn unless another was given. */
    val baseUrl: String

    override fun toString(): String

    /** 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 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. null or empty: every kind. */
    suspend fun geocode(
        q: String, limit: Int? = null, proximity: LonLat? = null, types: List<GeocodeType>? = null,
    ): GeocodeResponse

    /** The user's consent changed. Off drops every search not yet sent. On, searches go in batches of 50, on
     * [appInBackground] and on [flushTelemetry]. */
    fun setTelemetry(on: Boolean): Unit

    /** The user chose this result from a geocode list. It is kept on this device for offline search (placeCache),
     * and goes to telemetry only while telemetry is on. */
    fun pick(result: GeocodeResult)

    /** Places the user picked on this device, most picked first: a "your places" list. Empty with placeCache null. */
    fun places(): List<Place>

    /** The user asked to forget their places: clears memory and the store. */
    fun forgetPlaces()

    /** Sends what is waiting now, for example when an order is placed. null when telemetry is off or nothing waits.
     * A failed send throws and its searches are not kept: no retry, nothing stored on the device. */
    suspend fun flushTelemetry(): SearchTelemetryReceipt?

    /** The app went to the background: sends waiting searches without waiting for the answer. Call it from onStop
     * of your activity, or from a ProcessLifecycleOwner observer. Does nothing while telemetry is off. A failed send
     * is dropped. */
    fun appInBackground()

    /** 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 on disk. [source] gives the fixes (the app's location callback through a [PositionFeed]).
     * Throws trace_running when a trip is being recorded or still waits to be sent. */
    fun startTrace(consent: Consent, source: PositionSource, maxAccuracyM: Double = TRACE_MAX_ACCURACY_M)

    /** true while a trip is being recorded. */
    val 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] erases the trip. Empty when nothing was recorded or under 2 points. A failed send
     * throws and keeps the unsent points in memory: call stopTrace() again to retry, [cancelTrace] to drop them. */
    suspend fun stopTrace(): List<TraceReceipt>

    /** Stops recording and drops the trip unsent: the user changed their mind. */
    fun cancelTrace()

    /** One of your organization's trips as stored, GeoJSON [lon, lat, t]: the export. Needs trace:write. */
    suspend fun trace(id: String): Trace

    /** Erases a trip's points and times now. The id is kept, never reused. Twice is fine. Needs trace:write. */
    suspend fun deleteTrace(id: String): 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. */
    suspend fun traceCells(month: String, bbox: BBox): 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. kmH 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. */
    suspend fun traceSpeeds(month: String, bbox: BBox): 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. kmH 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; null,
     * all hours. rule["hours"] says which. [band] needs API 0.49.0.
     * Needs insights:read. */
    suspend fun traceRoads(month: String, bbox: BBox, band: RoadBand? = null): 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. */
    suspend fun ownTraceCells(month: String, bbox: BBox): 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. */
    suspend fun ownArrivals(month: String): 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. */
    suspend fun ownHours(month: String): 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. */
    suspend fun allHours(month: String): 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. */
    suspend fun traceKhoroos(month: String): 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 noKhoroo where no current khoroo polygon holds its end. Needs trace:read. */
    suspend fun ownKhoroos(month: String): 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. */
    suspend fun ownKinds(month: String): 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. null or empty: every kind.
     * [point] is sent as lon and lat; the API reads lng as lon too, for a hand-written request. */
    suspend fun reverse(
        point: LonLat, radius: Double? = null, limit: Int? = null, types: List<ReverseType>? = null,
    ): 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 [NearbyCategory.PHARMACY], "Хоол" RESTAURANT and CAFE. [radius] in
     * metres, 1000 when null, at most 10000; [limit] 10 when null, at most 20. Categories are sent once each, in the
     * order given; none is refused here with the API's 400 invalid_category: no request. A store comes with code null:
     * show "код алга". [open] true sends open=1: only places open now; those with no readable hours are counted in
     * hoursUnknown.
     * [point] is sent as lon and lat; the API reads lng as lon too, for a hand-written request. */
    suspend fun nearby(
        point: LonLat, categories: List<NearbyCategory>, radius: Double? = null, limit: Int? = null,
        open: Boolean = false, photo: Boolean = false,
    ): NearbyResponse

    /** The building entrances nearest a point, nearest first, each with its building and distance (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 null, at most 1000; [limit] 1 when null, at most
     * 20. Needs geocode:read. [point] is sent as lon and lat; the API reads lng as lon too, for a hand-written request. */
    suspend fun entrancesNear(point: LonLat, radius: Double? = null, limit: Int? = null): NearEntrances

    /** 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). */
    suspend fun directory(buildingId: String): BuildingDirectory

    /** 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. */
    suspend fun door(code: String, unit: String? = null): Door

    /** 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. */
    suspend fun registerDoorName(name: String, code: String): DoorNameReceipt

    /** Your organization's door names, A to Z, expired ones flagged until another organization takes them. Needs
     * names:write. */
    suspend fun doorNames(): OwnDoorNames

    /** The door a name names, as door() answers it, official address first. Any spelling of the name finds it.
     * 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. */
    suspend fun doorByName(name: String): 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. */
    suspend fun doorNameTransferCode(name: String): DoorNameTransfer

    /** Clears the waiting transfer code of one of your names. `cancelled` is false when none was waiting. Needs
     * names:write. */
    suspend fun cancelDoorNameTransfer(name: String): DoorNameTransferCancel

    /** Take a name with the transfer code its holder gave you. The door and expiresAt stay; the code is used up;
     * status is transferred. Needs names:write. 409 transfer_refused (wrong, cleared or timed out; counts a door
     * miss), 409 already_yours, 422 too_many. */
    suspend fun claimDoorName(name: String, transferCode: String): DoorNameReceipt

    /** 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. */
    suspend fun writeDoorNotes(name: String, instructionsMn: String? = null, instructionsEn: String? = null): 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. */
    suspend fun doorNotes(name: String): DoorNotes

    /** Send a gate photo for the door one of your names points at: the entrance as a guest sees it on arrival (API
     * 0.60.0). [jpeg] is one JPEG file as the camera saved it, sent as is: up to 2 MB, 320 to 8000 px a side. The API
     * removes its metadata (GPS, the phone, the time) before it stores it; the orientation stays. The photo waits for
     * a person to review it, who refuses one showing 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 until dock opens it). 400
     * not_jpeg, invalid_photo, photo_too_small, photo_too_big; 413 too_large; 404 not_found for a name your
     * organization does not hold or whose door was retired. A name the API would refuse is refused here with its 400:
     * no request. */
    suspend fun sendDoorPhoto(name: String, jpeg: ByteArray): DoorPhotoReceipt

    /** The gate photo of yours a person accepted last, for the door one of your names points at (API 0.61.0). Never a
     * waiting or refused one, never another organization's. Show it inside your app only, never on a public page of
     * the place. Pass the [sha256] of the photo you hold: when it is still the one served, the answer has jpeg null
     * and no bytes came. A sha256 that is not 64 hex characters is not sent: the photo comes whole. Needs names:write
     * and the hotel.notes feature (403 feature_off). 404 not_found for a name your organization does not hold, a
     * retired door, a restricted site, or no accepted photo. */
    suspend fun doorPhoto(name: String, sha256: String? = null): DoorPhoto

    /** One end of a route: a building code (routes to its street side or gate) or a point. */
    sealed class End {
        /** A building code: the route goes to the street side or the gate. */
        data class Code(val code: String) : End()

        /** A point. */
        data class Point(val point: LonLat) : End()
    }

    /** Route between two ends. A code routes to the street side of its building or the gate of its yard; `to.gapM` 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; sent in UTC. via (API 0.67.0): up to ROUTE_VIA_MAX ends passed on the
     * way, in order; 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]: ..."). */
    suspend fun route(
        from: End, to: End, mode: RouteMode? = null, departAt: Date? = null, via: List<End> = emptyList(),
    ): 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 a point as in route. `durationsS[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. */
    suspend fun matrix(from: List<End>, to: List<End>, mode: RouteMode? = null): 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. [from] is a building code (from its street side or gate) or a point
     * 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. */
    suspend fun isochrone(from: End, contoursMinutes: List<Int>, mode: RouteMode? = null): 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 points 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. */
    suspend fun matching(coordinates: List<LonLat>, timestamps: List<Long>? = null, mode: RouteMode? = null): 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 a point 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 `waypointIndex`, 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. */
    suspend fun optimizedTrips(stops: List<End>, roundtrip: Boolean? = null, mode: RouteMode? = null): 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; null or 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(code) takes a code. Anything the API would
     * refuse is refused here with its error: 404 not_found (sources), 400 invalid_point, invalid_radius,
     * invalid_limit; no request. Needs tiles:read; counts against rate_limit and daily_quota. Coverage: Ulaanbaatar. */
    suspend fun tilequery(
        point: LonLat, sources: List<TilequerySource> = listOf(TilequerySource.BUILDING, TilequerySource.ENTRANCE), radius: Double? = null,
        limit: Int? = null, geometry: TilequeryGeometry? = null,
    ): 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. idsSha256
     * changes when a closure is added or lifted. Needs route:read. */
    suspend fun closures(): 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] (UUID.randomUUID().toString() 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. */
    suspend fun reportClosure(report: ClosureReport, idempotencyKey: String? = null): Closure

    /** One of your organization's closure reports as stored, note included: your export. Needs closures:write.
     * 404 not_found for others' ids. */
    suspend fun closure(id: String): Closure

    /** The road is open again: lift one of your reports from [on] (YYYY-MM-DD Ulaanbaatar), null for today, 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. */
    suspend fun liftClosure(id: String, on: String? = null): Closure

    /** Tell dock a door is wrong. A person reviews it; door data does not change until then. Needs door:read. */
    suspend fun reportDoor(report: DoorReport): 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. */
    suspend fun place(id: String): Address

    /** 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 Date(0).
     * limit 1 to 1000, default 100, on every call: the cursor does not keep it. */
    suspend fun changes(since: Date?, limit: Int? = null, cursor: String? = null): ChangesResponse

    /** A driver on site says a door is there, elsewhere or gone. 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. */
    suspend fun confirm(confirmation: Confirmation): ConfirmReceipt

    /** 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. */
    suspend fun ownConfirmations(month: String): OwnConfirmations

    /** This key's organization: caps and the last [days] days (1 to 90). Free, answers after the quota. */
    suspend fun usage(days: Int? = null): UsageResponse

    /** TileJSON URL of a tile source (building, entrance), without the key. */
    fun tileJsonUrl(source: String): String

    /** Adds the key to a URL of this API only, as ?access_token=. Other hosts (a partner basemap, fonts) get nothing.
     * Use it where MapLibre Native asks for a resource URL. */
    fun authorizedUrl(url: String): String

    /** The dock style ([dockStyle]) as a string for MapLibre Native's Style.Builder().fromJson, its tile sources
     * keyed. Holds the key: do not log it. */
    fun styleJson(options: StyleOptions = StyleOptions()): String

    /** A style saved with POST /v1/styles or the style editor, keyed, for Style.Builder().fromUri. The API carries the
     * key into the style's sources and tiles. Throws IllegalArgumentException unless [styleId] is "st_" and 10
     * characters. */
    fun styleUrl(styleId: String): String
}

Basemap.kt ​

kotlin
/** An offline pack's manifest block `basemap` (null in packs built before 2026-10-03, or with no base map). */
data class PackBasemap(
    val method: String,
    val file: String,
    val format: String,
    val schema: String,
    val schemaVersion: String,
    val tileType: String,
    val tiles: Long,
    val minzoom: Int,
    val maxzoom: Int,
    val bounds: List<Double>,
    val layers: List<String>,
    val excludedLayers: List<String>,
    val languages: List<String>,
    val osmTime: String? = null,
    /** Show on the map: the source carries it, so MapLibre's attribution shows it. */
    val attribution: String,
    val rule: String,
    val ruleMn: String,
) {
    companion object {
        /** The block from the pack's manifest.json text; null when the pack has no base map. */
        fun fromManifest(text: String): PackBasemap?
    }
}

/**
 * @param lang label language, one of the block's languages ("mn" or "en"). A name missing in it falls back to OSM's
 *   name.
 * @param glyphs a glyphs URL ({fontstack}, {range}). The pack carries no fonts: without glyphs the style has no labels.
 * @param fonts font stack on that glyphs URL. Default Noto Sans Regular.
 * @param background background color. Default the dock style's paper tone.
 */
data class BasemapStyleOptions(
    val lang: String = "mn",
    val glyphs: String? = null,
    val fonts: List<String>? = null,
    val background: String? = null,
)

/** Layers the base map must not carry: business names (shown only when registered) and OSM house numbers. */
val BASEMAP_EXCLUDED: List<String>

/**
 * 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:// URL; the source
 * asks for it as pmtiles://, which MapLibre Native reads itself. MapLibre Native 13.6.1 draws nothing from an
 * asset:// URL: copy the file out of the APK first. Throws IllegalArgumentException for
 * no block, 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.
 */
fun basemapStyle(basemap: PackBasemap?, url: String, options: BasemapStyleOptions = BasemapStyleOptions()): JsonObject

Closures.kt ​

kotlin
/** Valhalla's exclude_polygons limits per route request, all rings together. */
data class ValhallaLimits(val perimeterM: Double, val vertices: Int)

/** One closure in GET /v1/closures. No partner name, no note. */
data class ClosureItem(
    /** cl-000001. Never reused. */
    val id: String,
    /** flood, washout, bridge_out, snow, mud, works or other. */
    val kind: String,
    /** First closed day, YYYY-MM-DD Ulaanbaatar. */
    val from: String,
    /** Last closed day, or null: until lifted. */
    val to: String?,
    /** One partner's word, not field checked. */
    val confidence: Double,
    val perimeterM: Double,
    /** Ring vertices, the closing one included. */
    val vertices: Int,
    /** Closed ring. One of Valhalla's exclude_polygons. */
    val ring: List<LonLat>,
) {
    /** from <= day, and day <= to when to is set. Days compare as YYYY-MM-DD strings. */
    fun isActive(day: String): Boolean
}

/** GET /v1/closures: every closure not over today, the same shape as an offline pack's closures block. */
data class ClosuresResponse(
    val method: String,
    val source: String,
    /** Today when it was fetched, YYYY-MM-DD Ulaanbaatar. */
    val asOf: String,
    val current: Int,
    /** sha256 of the sorted ids joined by commas; null for none. An offline pack's closures_sha256. */
    val idsSha256: String?,
    val bufferM: Double,
    val rule: String,
    val activeWhen: String,
    /** Per route request, all rings together. */
    val valhallaLimits: ValhallaLimits,
    val items: List<ClosureItem>,
)

/** What to send Valhalla for one route day: the rings of the closures active that day. */
data class ExcludePolygons(
    /** YYYY-MM-DD, Ulaanbaatar. */
    val day: String,
    /** The closures avoided, in list order. Show them with the route. */
    val ids: List<String>,
    /** Valhalla's exclude_polygons. */
    val rings: List<List<LonLat>>,
    val perimeterM: Double,
    val vertices: Int,
) {
    /**
     * Offline: the id of the first closure here 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).
     */
    fun closureAt(point: LonLat): String?
}

/** The day's closures are over Valhalla's limits for one request. Refuse the route: never drop a closure. */
class ClosuresOverLimitException(val day: String, count: Int, val perimeterM: Double, val vertices: Int, limits: ValhallaLimits) :
    RuntimeException(
        "dock: $count closures on $day: ${Math.round(perimeterM)} m of ring and $vertices vertices, " +
            "Valhalla takes ${limits.perimeterM.toLong()} m and ${limits.vertices}",
    )

/** Today in Ulaanbaatar (UTC+8, no daylight saving), YYYY-MM-DD. */
fun ubToday(now: Date = Date()): String

/**
 * 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 IllegalArgumentException for a bad day or a day
 * before the list's asOf (closures that ended before it are not in it: fetch again), and ClosuresOverLimitException
 * over Valhalla's limits.
 */
fun excludePolygons(closures: ClosuresResponse, day: String = ubToday()): ExcludePolygons

/** What is closed, in a report (POST /v1/closures). */
enum class ClosureKind(wire: String) {
    FLOOD("flood"),
    WASHOUT("washout"),
    BRIDGE_OUT("bridge_out"),
    SNOW("snow"),
    MUD("mud"),
    WORKS("works"),
    OTHER("other")
}

/** A closure's place: a point on the stretch (not on a junction), or a line of 2 or more points, at most 3000 m. */
sealed class ClosureGeometry {
    /** A point on the stretch, not on a junction. */
    data class Point(val point: LonLat) : ClosureGeometry()

    /** A line of 2 or more points, at most 3000 m. */
    data class LineString(val points: List<LonLat>) : ClosureGeometry()
}

/** POST /v1/closures. The sender is the key's organization. */
data class ClosureReport(
    val geometry: ClosureGeometry,
    val kind: ClosureKind,
    /** First closed day, YYYY-MM-DD Ulaanbaatar. null: today. */
    val from: String? = null,
    /** Last closed day. null: until lifted. */
    val to: String? = null,
    /** Your words for your own records, at most 200 characters; never served to others. No names, no phone numbers. */
    val note: String? = null,
)

/** A closure's fields, in Closure.properties. */
data class ClosureProperties(
    /** flood, washout, bridge_out, snow, mud, works or other. */
    val kind: String,
    val from: String,
    val to: String?,
    /** Open again from this day, YYYY-MM-DD; set once. null: not lifted. */
    val lifted: String?,
    /** As sent; "" for none. */
    val note: String,
    val confidence: Double,
    /** UTC ISO 8601. */
    val reportedAt: String,
)

/** One of your organization's closure reports, as stored (reportClosure, closure, liftClosure). */
data class Closure(
    /** "Feature". */
    val type: String,
    /** cl-000001. Never reused. */
    val id: String,
    val geometry: ClosureGeometry,
    val properties: ClosureProperties,
)

/** The API's Idempotency-Key rule (api/src/modules/closures/closure-input.ts IDEM_KEY). Use matches(), whole text. */
val IDEMPOTENCY_KEY_RE: Regex

Code.kt ​

kotlin
/** Digits and consonants: no vowels, no I, L, O, U. */
const val DOOR_CODE_ALPHABET: String

/** Characters in a building code. */
const val DOOR_CODE_LENGTH: Int

/** Why a text cannot be a building code. */
enum class DoorCodeError {
    /** Nothing typed. */
    EMPTY,

    /** Not 5 characters. */
    LENGTH,

    /** A character no code uses (a vowel, I, L, O, U, a Cyrillic letter with no Latin twin, a sign). */
    LETTERS
}

/** What [parseDoorCode] read. */
sealed class DoorCodeInput {
    /** The code, upper case Latin, and whatever followed it (flat number, household name) as typed, or null. */
    data class Code(val code: String, val unit: String?) : DoorCodeInput()

    /** [reason] is the API's invalid_code message, in English; show the person your own words for [error]. */
    data class Invalid(val error: DoorCodeError, val reason: String) : DoorCodeInput()
}

/** 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 [DoorCodeInput.Code] to door(): a code that finds no door counts
 * against the daily miss cap; one that cannot be a code is caught here first. */
fun 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. */
const val DOOR_LINK_BASE: String

/** 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. */
fun doorLink(text: String, unit: String? = null): String?

/** Reads the first door link in a text: a pasted link, a share message with a link in it, or the URL an app 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 [DoorCodeInput.Invalid] with the same reasons. No request. */
fun parseDoorLink(text: String): DoorCodeInput?

Crossings.kt ​

kotlin
/** Modelled monthly flow at a river crossing (lane D's dock_steppe.rivers). Rivers only: streams have none. */
data class CrossingFlow(
    val riverId: Long,
    /** 1991 to 2020 monthly normals, January to December, m3/s. */
    val normalM3s: List<Double>,
    /** Months 1 to 12 whose normal is at least 1.5 times the mean of the 12. */
    val highMonths: List<Int>,
    /** agrees, doubtful (the model reach is 10x below its OSM river's), unchecked. */
    val reach: String,
    /** 0.4, or LOW_FLOW_CONFIDENCE (0.1) on a doubtful reach and on snap none_fits or main_stem. */
    val confidence: Double,
    /**
     * 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 is taken. nearest: the nearest, no
     * lines to match on. null in packs built before the match.
     */
    val snap: String? = null,
)

/**
 * A delta channel: the river splits into a lake there and no model reach says what share a channel carries. The
 * crossing has no flow. Rule text in the pack's flow.delta.rule and rule_mn.
 */
data class CrossingDelta(
    /** The lake's OSM name, folded to one script when it mixed Latin and Cyrillic in a word. */
    val lake: String? = null,
    /** The lake's OSM element, as relation/114846. */
    val lakeOsm: String,
    /** The river whose mouths form the delta. */
    val river: String,
    /**
     * The lake's OSM name as written, when lake is folded. null when lake is the OSM text, and in packs before national
     * crossings v11. Show lake.
     */
    val lakeNameOsm: String? = null,
)

/**
 * 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.
 */
data class CrossingChannelOf(
    /** The river's OSM name, as on its riverWayId way. */
    val name: String,
    val nameEn: String? = null,
    /** The OSM way the name was read from. */
    val riverWayId: Long,
)

/**
 * 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.
 */
data class CrossingFlowsInto(
    /** The river's OSM name, as on its riverWayId way. */
    val name: String,
    val nameEn: String? = null,
    /** The OSM way the name was read from. */
    val riverWayId: Long,
    /** The fewest unnamed ways from this crossing's way to the river, its own included. */
    val ways: Int,
)

/** The OSM tags of the route ways at a crossing. Rule text in the pack's road_rule and road_rule_mn. */
data class CrossingRoad(
    /** The ways' OSM highway tags, as track or trunk. */
    val highway: List<String>,
    /** The ways' OSM surface tags; empty when none has one. */
    val surface: List<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.
     */
    val 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. */
const val LOW_FLOW_CONFIDENCE: Double

/** A GeoJSON Point: type "Point" and [lon, lat]. The geometry of a crossing, a pass or a place in a pack. */
data class CrossingGeometry(val type: String, val coordinates: LonLat)

/** A crossing's fields, in CrossingFeature.properties. */
data class CrossingProperties(
    /** ford: OSM says ford. none: OSM has no bridge or ford there, not that there is none. */
    val kind: String,
    /** river, stream, canal, or unknown on a ford node with no waterway line (fordNodeId set). */
    val waterway: String,
    val name: String? = null,
    val nameEn: String? = null,
    val intermittent: Boolean,
    /** null for streams and canals, and in packs built before the flow block. */
    val flow: CrossingFlow? = null,
    /** Set on a delta channel, with flow null. null in packs built before the delta rule. */
    val delta: CrossingDelta? = null,
    /** Set on an unnamed channel of a named river. null in packs built before the channel rule. */
    val channelOf: CrossingChannelOf? = null,
    /** Set on an unnamed river or stream that reaches one named river. null in packs built before the tributary rule. */
    val flowsInto: CrossingFlowsInto? = null,
    /** null on a new track alone (no OSM way), and in packs built before the road rule. */
    val 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
     * and in packs built before the ford node rule.
     */
    val fordNodeId: Long? = null,
    /**
     * The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. null when
     * name is the OSM text. Show name. Packs before offline v13 lack it.
     */
    val nameOsm: String? = null,
)

/** One crossing point in an offline pack's crossings.geojson (files[1]). */
data class CrossingFeature(val type: String, val geometry: CrossingGeometry, val properties: CrossingProperties)

/** The pack's flow block: the attribution to show with a high water month. */
data class CrossingsFlowBlock(val attribution: String)

/** An offline pack's crossings.geojson. Check its sha256 against the manifest's files[1] before use. */
data class PackCrossings(
    val type: String,
    /** A route passes a crossing when its line comes this close, m. */
    val matchM: Double,
    val note: String,
    val noteMn: String,
    /** null in packs built before the flow block. */
    val flow: CrossingsFlowBlock? = null,
    val features: List<CrossingFeature>,
    /** Crossings with road.paved true. null in packs built before the road rule, as the two rule texts. */
    val onPaved: Int? = null,
    val roadRule: String? = null,
    val roadRuleMn: String? = null,
    /**
     * None crossings that became the crossing of a bridge drawn just off the water, so left out of the pack. null in
     * packs built before the bridge rule, as the two rule texts.
     */
    val nearBridge: Int? = null,
    val bridgeRule: String? = null,
    val bridgeRuleMn: String? = null,
    /**
     * Crossings joined into the one beside them because their water way is split into two ways at the road. null in
     * packs built before the split rule, as the two rule texts.
     */
    val onePoint: Int? = null,
    val splitRule: String? = null,
    val splitRuleMn: String? = null,
    /**
     * Crossings marked intermittent from OSM seasonal=* alone, no intermittent tag. null in packs built before that
     * rule, as the two rule texts.
     */
    val fromSeasonal: Int? = null,
    val intermittentRule: String? = null,
    val intermittentRuleMn: String? = null,
    /**
     * Fords from an OSM ford node alone, no waterway line drawn. null in packs built before that rule, as the two rule
     * texts.
     */
    val fordNodes: Int? = null,
    val fordNodeRule: String? = null,
    val fordNodeRuleMn: String? = null,
    /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. null in packs before offline
     * v13. */
    val namesFolded: Int? = null,
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackCrossings
    }
}

/** A ford or unbridged crossing the route passes. */
data class CrossingWarning(
    /** ford or none. */
    val kind: String,
    val point: LonLat,
    val waterway: String,
    val name: String?,
    val nameEn: String?,
    val intermittent: Boolean,
    /** Along the route to its point nearest the crossing, m, rounded. A tie goes to the earlier pass. */
    val alongM: Double,
    /** From the route line, m, one decimal. */
    val offM: Double,
    /** The month is one of highMonths. null when the crossing has no flow (streams, canals). */
    val highWater: Boolean?,
    val highMonths: List<Int>,
    /**
     * 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.
     */
    val monthM3s: Double?,
    val reach: String?,
    val confidence: Double?,
    /** The flow's snap. null with no flow or in a pack built before the match. */
    val snap: String? = null,
    /** A delta channel: say "delta channel, flow unknown". null elsewhere and in packs built before the delta rule. */
    val 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.
     */
    val channelOf: CrossingChannelOf? = null,
    /**
     * An unnamed river or stream that reaches a named river: say "unnamed, flows into <name>". channelOf wins when both
     * are set. null elsewhere and in packs built before the tributary rule.
     */
    val flowsInto: 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.
     */
    val 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.
     */
    val fordNodeId: Long? = null,
    /** The OSM text when name was folded to one script, else null. Show name. */
    val nameOsm: String? = null,
)

/** What crossingsOnRoute returns: the warnings in route order, with the pack's note. */
data class RouteCrossings(
    /** 1 to 12. */
    val month: Int,
    val matchM: Double,
    /** In route order. */
    val warnings: List<CrossingWarning>,
    /** Show with the warnings: from OSM tags, not field checked. */
    val note: String,
    val noteMn: String,
    /** Show when a warning has flow; else null. */
    val flowAttribution: String?,
    /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. null when the pack has no rule. */
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
)

/** This month in Ulaanbaatar (UTC+8), 1 to 12. crossingsOnRoute's default month. */
fun ubMonth(now: Date = Date()): Int

/**
 * 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 highWater. Throws IllegalArgumentException for a
 * line under 2 points or a bad month.
 */
fun crossingsOnRoute(crossings: PackCrossings, line: List<LonLat>, month: Int = ubMonth()): RouteCrossings

DoorNames.kt ​

kotlin
/** A door name your organization holds, as POST /v1/door-names and the claim answer it. */
data class DoorNameReceipt(
    /** As registered, spaces collapsed. */
    val name: String,
    /** The door it names. */
    val code: String,
    val registeredAt: String,
    /** After this the name finds nothing and any organization may take it. */
    val expiresAt: String,
    /** registered: yours from now. moved: your name now names this door. unchanged: it already did. A claim answers
     * transferred. */
    val status: String,
)

/** One of your organization's names in doorNames(). */
data class OwnDoorName(
    val name: String,
    val code: String,
    val registeredAt: String,
    val expiresAt: String,
    /** true after expiresAt: it finds nothing, and stays listed until another organization takes it. */
    val expired: Boolean,
    /** When the transfer code you made stops working (API 0.53.0); null when none is waiting. */
    val transferUntil: String? = null,
)

/** GET /v1/door-names: your organization's names, A to Z. */
data class OwnDoorNames(
    /** Unexpired names one organization may hold at once. */
    val limit: Int,
    val names: List<OwnDoorName>,
)

/** GET /v1/door-names/{name}: the door as door() answers it, official address first. The holder is never shown. */
data class NamedDoor(
    val name: String,
    val expiresAt: String,
    val door: Door,
)

/** POST /v1/door-names/{name}/transfer-code. The code is shown once, never again. */
data class DoorNameTransfer(
    val name: String,
    /** dnt_ and 24 characters. Anyone holding it can claim the name: give it only to the organization taking it. */
    val transferCode: String,
    /** Refused after this: 7 days, or the name's expiresAt if sooner. */
    val transferUntil: String,
)

/** DELETE /v1/door-names/{name}/transfer-code. cancelled: a waiting code was cleared; false: none was waiting. */
data class DoorNameTransferCancel(
    val name: String,
    val cancelled: Boolean,
)

/** POST /v1/door-names/{name}/notes (API 0.58.0, feature hotel.notes): your waiting note. */
data class DoorNoteReceipt(
    /** The waiting note; writing again keeps it. */
    val id: String,
    /** queued: a new note waits for review. updated: your waiting note has this text now. unchanged: it had. */
    val status: String,
    val name: String,
    /** The door the name points at, which the note is for. */
    val code: String,
    val instructionsMn: String? = null,
    val instructionsEn: String? = null,
    /** When the waiting text was last written. */
    val receivedAt: String,
    /** review: a person reviews it. Until then door() serves the notes the map holds, not this one. */
    val next: String,
)

/** One of your notes in doorNotes(). */
data class DoorNoteState(
    val id: String,
    /** waiting: not reviewed, guests do not see it. accepted: door() serves the languages it gave. refused: not used. */
    val state: String,
    val instructionsMn: String? = null,
    val instructionsEn: String? = null,
    /** When this text was last written. */
    val receivedAt: String,
    /** When a person accepted or refused it; null while it waits. */
    val reviewedAt: String? = null,
)

/** GET /v1/door-names/{name}/notes (API 0.59.0): your organization's notes for the door, never another's. */
data class DoorNotes(
    val name: String,
    /** The door the name points at now. */
    val code: String,
    /** The waiting one first, then the newest review, at most 20. Empty: you wrote none. */
    val notes: List<DoorNoteState>,
)

/** POST /v1/door-names/{name}/photo (API 0.60.0, feature hotel.notes): your waiting gate photo. */
data class DoorPhotoReceipt(
    /** The waiting photo; sending again keeps it. */
    val id: String,
    /** queued: a new photo waits for review. updated: your waiting photo is now this one. unchanged: it was. */
    val status: String,
    val name: String,
    /** The door the name points at, which the photo is for. */
    val code: String,
    /** SHA-256 of the photo as stored, after its metadata was removed: not the hash of the file you sent. doorPhoto()
     * answers it as the sha256 of the photo once a person accepts it. */
    val sha256: String,
    /** Size as stored, in bytes. */
    val bytes: Int,
    /** Pixels as stored, before the EXIF orientation turns it. */
    val width: Int,
    val height: Int,
    /** When the waiting photo was last sent. */
    val receivedAt: String,
    /** review: a person reviews the photo. Once accepted, doorPhoto() serves it. */
    val next: String,
)

/** GET /v1/door-names/{name}/photo (API 0.61.0): your accepted gate photo. [jpeg] null means unchanged: the photo you
 * hold with this [sha256] is still the one served, and no bytes came. */
class DoorPhoto(
    /** SHA-256 of the JPEG served, 64 lower case hex: the ETag, and the sha256 the send answered. */
    val sha256: String,
    /** The JPEG as stored, metadata removed, orientation kept; null when unchanged. */
    val jpeg: ByteArray?,
) {
    override fun equals(other: Any?): Boolean
    override fun hashCode(): Int

    /** No bytes: the size only. */
    override fun toString(): String
}

Format.kt ​

kotlin
/** Language of a label. */
enum class Lang {
    MN,
    EN
}

/**
 * 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.md 4d.
 */
fun label(door: Door, lang: Lang = Lang.MN): String

/** The same for the door at one end of a route: the spoken form, then the code. */
fun label(door: RouteDoor, lang: Lang = Lang.MN): String

/** The same for a geocode result: the name, then the long form, then the code. */
fun label(place: GeocodeResult, lang: Lang = Lang.MN): String

/** The same for a reverse result: the name, then the long form, then the code. */
fun label(place: ReverseResult, lang: Lang = Lang.MN): String

/** The same for an address field answer: the name, then the long form, then the code. */
fun label(place: Address, lang: Lang = Lang.MN): String

Pack.kt ​

kotlin
/** One row of an offline pack manifest's `files`: a file in the pack folder, its size and sha256. */
data class PackFile(
    val name: String,
    val bytes: Long,
    /** Lower case hex. */
    val sha256: String,
)

/** The part of an offline pack's manifest.json [checkPack] reads. */
data class PackManifestFiles(val files: List<PackFile>) {
    companion object {
        /** The whole manifest.json's text; only `files` is read. */
        fun fromJson(text: String): PackManifestFiles
    }
}

/** OK: size and sha256 match. MISSING: no such file. BYTES: another size, not hashed. SHA256: same size, other bytes. */
enum class PackFileStatus {
    OK,
    MISSING,
    BYTES,
    SHA256
}

/** One file of a pack manifest as checkPack found it. */
data class PackFileCheck(
    val name: String,
    val status: PackFileStatus,
    /** The file's size, null when missing. */
    val bytes: Long?,
    val expectedBytes: Long,
    /** The file's sha256, null when missing or another size. */
    val sha256: String?,
    val expectedSha256: String,
)

/** What checkPack returns. */
data class PackCheck(
    /** Every listed file is there with its size and sha256. Use no file from the pack otherwise. */
    val ok: Boolean,
    /** In manifest order. */
    val files: List<PackFileCheck>,
    /** Names of the files not ok, in manifest order. Download them again. */
    val bad: List<String>,
)

/**
 * Offline: checks each file the manifest lists in [directory], in its order, by size and then sha256, reading 1 MB at
 * a time. A folder where a file is listed is missing. Files the manifest does not list are not read. Blocks while it
 * hashes (the base map is over 100 MB): call it off the main thread, as on Dispatchers.IO. Throws
 * IllegalArgumentException for a manifest with no files, a name that is not a plain file name, a name listed twice,
 * a negative bytes value, or a sha256 that is not 64 lower case hex; a file that cannot be read throws its own error.
 */
fun checkPack(manifest: PackManifestFiles, directory: File): PackCheck

PackRoute.kt ​

kotlin
/** What every route on a pack sends: costing auto, costing_options.auto.use_tracks 0.5 today. Keys as written. */
data class PackRouteRequest(
    val costing: String,
    /** Valhalla's costing_options, as in the manifest. null when it has none. */
    val costingOptions: JsonObject? = null,
)

/** The part of an offline pack's manifest.json [packRoute] reads. */
data class PackRouteManifest(
    val id: String,
    /** [west, south, east, north]. */
    val bbox: List<Double>,
    val request: PackRouteRequest,
    /** scored, or osm_only: no trace scores yet. */
    val confidence: String,
    val attribution: String,
    val note: String,
    val closures: ClosuresResponse,
    /** What [confidence] means, for the driver, en and mn. Packs from offline v10; null before. */
    val confidenceNote: String? = null,
    val confidenceNoteMn: String? = null,
) {
    companion object {
        /** The whole manifest.json's text. costingOptions keeps its keys as written (use_tracks). */
        fun fromJson(text: String): PackRouteManifest
    }
}

/** What packRoute returns: the Valhalla request and what to show with the route. */
data class PackRoute(
    /**
     * POST it to /route on your own Valhalla, the pack's tiles.tar as mjolnir.tile_extract: locations, costing,
     * costing_options, exclude_polygons, units km.
     */
    val body: JsonObject,
    /** The manifest's id. */
    val pack: String,
    val day: String,
    /** The closures the body avoids, in list order. Show them with the route. */
    val closures: List<String>,
    /** The pack's route costs come from trace scores. */
    val scored: Boolean,
    val confidence: String,
    /** Show it with every route. */
    val attribution: String,
    val note: String,
    /** Say it to the driver when the pack has no trace scores: [confidenceNote], en and mn; null when scored. */
    val traceNote: String?,
    val traceNoteMn: String?,
    /**
     * What [confidence] means, for the driver: the manifest's confidenceNote and confidenceNoteMn when it has both;
     * else [PACK_TRACE_NOTE] and [PACK_TRACE_NOTE_MN] when the pack is not scored; else null.
     */
    val confidenceNote: String?,
    val confidenceNoteMn: String?,
)

/** A stop inside a closure ring: Valhalla has no road there. Refuse the route, as GET /v1/route does (422 end_in_closure). */
class EndInClosureException(val stop: Int, val closure: String) :
    RuntimeException("dock: stop $stop is inside closure $closure: no road there")

/** What to say when a pack has no trace scores (confidence osm_only), in English. */
const val PACK_TRACE_NOTE: String

/** The same in Mongolian. */
const val PACK_TRACE_NOTE_MN: String

/**
 * Offline: the Valhalla request for a route through [stops] (2 or more) on the pack: the manifest's costing and
 * costingOptions, the route day's closure rings as exclude_polygons (from [closures], a newer GET /v1/closures answer,
 * else the pack's block), units km. Throws IllegalArgumentException for a manifest with no costing or bbox, under 2
 * stops, a stop that is not a finite lon, lat or is outside the pack's bbox, and for the day as [excludePolygons];
 * ClosuresOverLimitException; EndInClosureException for a stop inside an avoided ring.
 */
fun packRoute(
    manifest: PackRouteManifest,
    stops: List<LonLat>,
    day: String = ubToday(),
    closures: ClosuresResponse? = null,
): PackRoute

Passes.kt ​

kotlin
/** A pass's fields, in PassFeature.properties. */
data class PassProperties(
    /** mountain_pass when OSM has that tag, else saddle. */
    val kind: String,
    val name: String? = null,
    /**
     * The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. null when
     * name is the OSM text. Show name. Packs before offline v12 lack it.
     */
    val nameOsm: String? = null,
    val nameEn: String? = null,
    /** The OSM ele tag read as metres. null when OSM has none, or only other text, as "7000 ft": see eleOsm. */
    val eleM: Double? = null,
    /** The OSM ele tag as written. */
    val eleOsm: String? = null,
    /** The OSM node id. */
    val nodeId: Long,
    /** The OSM car ways through the node. Empty on a new track alone. */
    val routeWayIds: List<Long>,
    val newTrack: Boolean,
    /** The ways' OSM highway and surface tags, as on crossings. null on a new track alone. */
    val road: CrossingRoad? = null,
    /**
     * The Copernicus GLO-30 DEM height of the cell the node falls in, whole metres: a second source beside eleM, never
     * mixed into it. null when the pack had no DEM tile there; packs built before 2026-10-03 lack it.
     */
    val demM: Double? = null,
    /**
     * True when eleM and demM are both there and more than dem.differM (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.
     */
    val heightsDiffer: Boolean? = null,
)

/** The pack's DEM block: where demM comes from. Show attribution wherever a DEM height shows. */
data class PassDem(
    val source: String,
    val attribution: String,
    val rule: String,
    val ruleMn: String,
    val note: String,
    val noteMn: String,
    /** DEM tiles read. */
    val tiles: Int,
    /** Tiles the build had no copy of: passes there have demM null. */
    val missing: List<String>,
    /** Heights further apart than this, m, set heightsDiffer. Packs before passes v3 lack it and the three below. */
    val differM: Double? = null,
    val differRule: String? = null,
    val differRuleMn: String? = null,
    /** Passes with heightsDiffer true, all Mongolia. */
    val differ: Int? = null,
)

/** One pass in an offline pack's passes.geojson: an OSM mountain_pass=yes or natural=saddle node of a car way. */
data class PassFeature(val type: String, val geometry: CrossingGeometry, val properties: PassProperties)

/**
 * 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.
 */
data class PackPasses(
    val type: String,
    /** Passes in the file, all Mongolia. */
    val written: Int,
    /** Per kind, all Mongolia. */
    val counts: Map<String, Int>,
    val named: Int,
    /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. null in packs before offline
     * v12. */
    val namesFolded: Int? = null,
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
    /** Passes with a height in metres. */
    val withEle: Int,
    /** A route goes over a pass when its line comes this close, m. */
    val matchM: Double,
    val rule: String,
    val ruleMn: String,
    val source: String,
    val note: String,
    val noteMn: String,
    val features: List<PassFeature>,
    /** Passes with a DEM height. null in packs built before 2026-10-03. */
    val withDem: Int? = null,
    /** null in packs built before 2026-10-03. */
    val dem: PassDem? = null,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackPasses
    }
}

/** A pass the route goes over. */
data class PassOnRoute(
    /** mountain_pass or saddle. */
    val kind: String,
    val point: LonLat,
    val name: String?,
    val nameEn: String?,
    val eleM: Double?,
    val eleOsm: String?,
    val nodeId: Long,
    val newTrack: Boolean,
    val road: CrossingRoad?,
    /** Along the route to its point nearest the pass, m, rounded. A tie goes to the earlier pass. */
    val alongM: Double,
    /** From the route line, m, rounded to 0.1. */
    val offM: Double,
    /** The DEM height, whole metres, beside eleM. null when the pack has none for this pass. Label it DEM. */
    val demM: Double? = null,
    /**
     * The pack's flag: true when eleM and demM disagree by more than dem.differM. null when either is missing or the
     * pack has no flag. Show both heights and the dem differRule when true.
     */
    val heightsDiffer: Boolean? = null,
    /** The OSM text when name was folded to one script, else null. Show name. */
    val nameOsm: String? = null,
)

/** What passesOnRoute returns. */
data class RoutePasses(
    val matchM: Double,
    /** In route order. */
    val passes: List<PassOnRoute>,
    /** The highest eleM of these passes, m. null when none has a height: not "flat", OSM has no number. */
    val topM: Double?,
    /** Show with the passes: from OSM tags, not field checked. */
    val note: String,
    val noteMn: String,
    val rule: String,
    val ruleMn: String,
    /** The highest demM of these passes, m. null when none has one. Not mixed with topM. */
    val topDemM: Double? = null,
    /** The pack's DEM block, null when the pack has none. Show its attribution with a DEM height. */
    val dem: PassDem? = null,
    /** How many of these passes have heightsDiffer true. null when the pack has no flag (no dem.differRule): not
     * checked, not 0. */
    val differ: Int? = null,
    /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. null when the pack has no rule. */
    val nameRule: String? = null,
    val nameRuleMn: 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 matchM (5 m) of the pass node, as lane D's route
 * pages. Throws IllegalArgumentException for a line under 2 points.
 */
fun passesOnRoute(passes: PackPasses, line: List<LonLat>): RoutePasses

Places.kt ​

kotlin
/** Default number of places kept. The least picked, then the longest unpicked, go first. */
const val PLACES_MAX: Int

/** Key in the partner's store. A new format gets a new key. Same key and format as the JS SDK. */
const val PLACES_KEY: String

/** Where kept places live across app launches: SharedPreferences (see the Android page), a test map. */
interface PlaceStore {
    /** The value kept under [key], null when none. */
    fun getString(key: String): String?

    /** Keeps [value] under [key], replacing the old one. A thrown exception is ignored: memory still answers. */
    fun putString(key: String, value: String)

    /** Forgets [key]. */
    fun remove(key: String)
}

/** @param max 1 to 1000. @param store keeps the places across app launches; null: memory only, gone with the client. */
data class PlaceCacheOptions(val max: Int = PLACES_MAX, val store: PlaceStore? = null)

/** A place the user picked: the fields of a geocode result without score, layer and near, which belong to one
 * answer, not to the place. */
data class Place(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    val code: String?,
    val kind: String,
    /** Stable id. Never show it to people as an address. */
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val confidence: Int?,
    val source: String,
    val sourceDate: String? = null,
    val floor: Int? = null,
    val flat: String? = null,
    val claimed: Boolean? = null,
    val buildingType: String? = null,
) {
    constructor(r: GeocodeResult)
}

Pois.kt ​

kotlin
/** A place's fields, in PoiFeature.properties. */
data class PoiProperties(
    /** fuel or ger_camp. */
    val kind: String,
    /** The OSM element, N or W and the id, as N123. */
    val osm: String,
    /** Clean name from the state registry match. */
    val name: String,
    val nameEn: String? = null,
    /** State registration number. */
    val regNumber: String,
)

/** One registered place in an offline pack's pois.geojson. Unregistered places are never in the file. */
data class PoiFeature(val type: String, val geometry: CrossingGeometry, val properties: PoiProperties)

/** Places of one kind, all Mongolia: matched to an official registration, and not. */
data class PoiKindCount(val registered: Int, val unregistered: Int)

/** Per kind, all Mongolia. */
data class PoiCounts(val fuel: PoiKindCount? = null, val gerCamp: PoiKindCount? = null)

/** An offline pack's pois.geojson (files[2]; index.json pois_sha256). Check its sha256 against the manifest before use. */
data class PackPois(
    val type: String,
    /** false: the registry could not be read when the pack was built, so no place is listed. */
    val registryChecked: Boolean,
    val counts: PoiCounts,
    val written: Int,
    val rule: String,
    val ruleMn: String,
    val source: String,
    val note: String,
    val noteMn: String,
    val features: List<PoiFeature>,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackPois
    }
}

/** A registered fuel station or ger camp near the route. */
data class PoiOnRoute(
    /** fuel or ger_camp. */
    val kind: String,
    val point: LonLat,
    val osm: String,
    val name: String,
    val nameEn: String?,
    val regNumber: String,
    /** Along the route to its point nearest the place, m, rounded. A tie goes to the earlier pass. */
    val alongM: Double,
    /** From the route line, m, rounded. */
    val offM: Double,
)

/** What poisOnRoute returns. */
data class RoutePois(
    val nearM: Double,
    /** Registered places only, in route order. */
    val pois: List<PoiOnRoute>,
    /** false: no place was listed because the registry was not read; say so, do not say "none". */
    val registryChecked: Boolean,
    /**
     * Unregistered fuel stations in the pack, all Mongolia, not near this route: the pack has no place for them. An
     * empty pois is not "no fuel on the route" while this is above 0.
     */
    val unlistedFuel: Int,
    /** Unregistered ger camps in the pack, all Mongolia, as unlistedFuel. */
    val unlistedGerCamp: Int,
    /** Show with the places: from OSM, not field checked; unregistered places are counted, not shown. */
    val note: String,
    val noteMn: String,
)

/** lane D's route pages: fuel and ger camps within 2 km of the line. */
const val POI_NEAR_M: Double

/**
 * 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 IllegalArgumentException for a line under 2 points or a nearM not
 * above 0.
 */
fun poisOnRoute(pois: PackPois, line: List<LonLat>, nearM: Double = POI_NEAR_M): RoutePois

Qr.kt ​

kotlin
/**
 * 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.
 */
data class DoorQr(val link: String, val version: Int, val size: Int, val modules: List<List<Boolean>>)

/**
 * A door QR as ARGB pixels, black on white, [side] a side, row by row: for
 * `Bitmap.createBitmap(argb, side, side, Bitmap.Config.ARGB_8888)`.
 */
class DoorQrPixels(val link: String, val side: Int, val argb: IntArray)

/**
 * 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.
 */
fun doorQr(text: String, unit: String? = null): DoorQr?

/**
 * The QR of a door as an SVG document, black on white, for a WebView, a share sheet or print. null as doorQr. Each
 * module is [px] wide; [margin] light modules around it (4, the standard's minimum quiet zone). The SVG is
 * (size + 2 * margin) * px a side. Throws IllegalArgumentException for px not above 0 or a margin under 0.
 */
fun doorQrSvg(text: String, unit: String? = null, px: Double = 4.0, margin: Int = 4): String?

/**
 * The QR of a door as pixels, [px] a module and [margin] light modules around it, for an Android Bitmap. null as
 * doorQr. Throws IllegalArgumentException for px under 1 or a margin under 0.
 */
fun doorQrPixels(text: String, unit: String? = null, px: Int = 8, margin: Int = 4): DoorQrPixels?

Sand.kt ​

kotlin
/** A GeoJSON LineString: the stretch of the way inside the sand area. */
data class SandGeometry(val type: String, val coordinates: List<LonLat>)

/** A stretch's fields, in SandFeature.properties. */
data class SandProperties(
    /** The area's OSM element, as way/123 or relation/123. */
    val area: String,
    /** The area's OSM name. */
    val name: String? = null,
    /**
     * The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. null when
     * name is the OSM text. Show name. Packs before offline v11 lack it.
     */
    val nameOsm: String? = null,
    /** The stretch's length, km. */
    val km: Double,
    /** The car way's OSM id. null on a new track alone. */
    val wayId: Long? = null,
    val newTrack: Boolean,
    /** The way's OSM highway and surface tags, as on crossings. null on a new track alone. */
    val road: CrossingRoad? = null,
)

/** One stretch in an offline pack's sand.geojson: the part of one car way inside an OSM natural=sand area. */
data class SandFeature(val type: String, val geometry: SandGeometry, val properties: SandProperties)

/**
 * 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.
 */
data class PackSand(
    val type: String,
    /** Stretches in the file, all Mongolia. */
    val written: Int,
    val km: Double,
    /** OSM sand areas the stretches lie in. */
    val areas: Int,
    /** Stretches in a named area. */
    val named: Int,
    /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. null in packs before offline
     * v11. */
    val namesFolded: Int? = null,
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
    /** Stretches on a paved road (road.paved true), as town streets drawn inside sand. */
    val onPaved: Int,
    /** A route runs on a stretch where its line comes this close, m. */
    val matchM: Double,
    val rule: String,
    val ruleMn: String,
    val source: String,
    val note: String,
    val noteMn: String,
    val features: List<SandFeature>,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackSand
    }
}

/** A stretch through OSM sand the route runs on. */
data class SandOnRoute(
    val area: String,
    val name: String?,
    /** The whole stretch, km, from the pack. The route may run on part of it: onM. */
    val km: Double,
    val wayId: Long?,
    val newTrack: Boolean,
    val road: CrossingRoad?,
    /** Along the route to where it meets the stretch, m, rounded. */
    val alongM: Double,
    /** Metres of the stretch the route runs on, rounded. */
    val onM: Double,
    /** The stretch's first point the route meets, in route order. */
    val point: LonLat,
    /** The OSM text when name was folded to one script, else null. Show name. */
    val nameOsm: String? = null,
)

/** What sandOnRoute returns. */
data class RouteSand(
    val matchM: Double,
    val minOnM: Double,
    /** In route order. */
    val stretches: List<SandOnRoute>,
    /** Sum of onM, m. */
    val onM: Double,
    /** Show with the stretches: from OSM tags, not field checked. */
    val note: String,
    val noteMn: String,
    val rule: String,
    val ruleMn: String,
    /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. null when the pack has no rule. */
    val nameRule: String? = null,
    val nameRuleMn: 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. */
const val SAND_MIN_ON_M: Double

/**
 * 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 matchM; a piece is on the route when both
 * its ends lie within matchM 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 IllegalArgumentException for a line under 2 points.
 */
fun sandOnRoute(sand: PackSand, line: List<LonLat>): RouteSand

Style.kt ​

kotlin
/** Ulaanbaatar, Sükhbaatar Square. */
val UB_CENTER: LonLat

/** A saved style id: "st_" and 10 characters of the key alphabet. */
val STYLE_ID_RE: Regex

/**
 * @param background background color. Default a light paper tone.
 * @param building building fill (or extrusion) color. Default a warm grey.
 * @param buildingOutline building outline color.
 * @param extrude extrude buildings by height_m (or levels x 3 m). Default false: flat 2D.
 */
data class StyleOptions(
    val background: String? = null,
    val building: String? = null,
    val buildingOutline: String? = null,
    val extrude: Boolean = false,
)

/** Sources named dock-building and dock-entrance, TileJSON from the API, without the key. */
fun dockSources(baseUrl: String): JsonObject

/**
 * 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).
 */
fun dockLayers(options: StyleOptions = StyleOptions()): JsonArray

/** A whole style: background, dock buildings, dock entrances. Source URLs without the key: see Client.styleJson. */
fun dockStyle(baseUrl: String, options: StyleOptions = StyleOptions()): JsonObject

Suggest.kt ​

kotlin
/** PLAN 18: "suggestions from the third character". */
const val SUGGEST_MIN_CHARS: Int

/** Wait after the last key before asking, ms. Under the 250 ms people notice, over most gaps between keys. */
const val SUGGEST_DELAY_MS: Long

/** A door read from a typed code, or a geocode result. */
sealed class Suggestion {
    /** The door a typed code reads as. */
    data class Door(val door: mn.dock.Door) : Suggestion()

    /** A geocode result. */
    data class Result(val result: GeocodeResult) : Suggestion()
}

/** What to show a person for a suggestion. Anything without a matched official address ends in "(албан бус)" /
 * "(unofficial)". */
fun label(item: Suggestion, lang: Lang = Lang.MN): String

/**
 * Text in, suggestions out. [input] on every change of the field; [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. An empty list
 * for a text of [minChars] or more means no match. Call [input], [cancel] and [choose] from one thread, the main one.
 *
 * @param scope where requests run and [show] and [fail] are called: the screen's scope, on Dispatchers.Main.
 * @param limit geocode results per list, 1 to 20. Default 5. A door from a typed code comes on top of them.
 * @param proximity asked on each request: the person or the map centre. Nearer places first.
 * @param types only these kinds, as geocode types: a street picker passes listOf(STREET). null or
 *   empty: every kind. A typed code or a pasted door link is looked up as a door only when types has DOOR.
 * @param show the list and the text it is for.
 * @param fail a DockError or another failure of the request.
 */
class Suggester(
    client: Client,
    scope: CoroutineScope,
    limit: Int = SUGGEST_LIMIT,
    proximity: (() -> LonLat?)? = null,
    types: List<GeocodeType>? = null,
    minChars: Int = SUGGEST_MIN_CHARS,
    delayMs: Long = SUGGEST_DELAY_MS,
    show: (List<Suggestion>, String) -> Unit,
    fail: (Throwable) -> Unit,
) {
    /** Reads [text]: from minChars characters, asks delayMs after the last call; under them, empties the list. */
    fun input(text: String)

    /** Stops the wait and the request in flight. Their answers are dropped. */
    fun cancel()

    /** The person chose this suggestion. Stops what is in flight; a geocode result goes to [Client.pick] (offline
     * places, opted-in telemetry), a door from a code does not. Show label(item) in the field. */
    fun choose(item: Suggestion)
}

Telemetry.kt ​

kotlin
/** API limits (api/src/modules/telemetry/telemetry-input.ts). */
const val TELEMETRY_BATCH: Int

/** Characters of typed text kept per search, counted as the API counts them (code points). The rest is cut. */
const val TELEMETRY_MAX_CHARS: Int

/** Results kept per search. */
const val TELEMETRY_MAX_RESULTS: Int

/** What the user picked: kind and id as geocode gave them, rank 1 = the first result shown. */
data class SearchPick(val kind: String, val id: String, val rank: Int)

/** One search as the API takes it. Text, count and pick only: no time, point, device or session. */
data class SearchEvent(val q: String, val results: Int, val picked: SearchPick? = null)

Trace.kt ​

kotlin
/** API limits (api/src/modules/traces/trace-input.ts). */
const val TRACE_FORMAT: String

/** Points per upload. stopTrace() splits a longer trip. */
const val TRACE_MAX_POINTS: Int

/** Fixes worse than this, in meters, are left out. Accuracy itself is never sent. */
const val TRACE_MAX_ACCURACY_M: Double

/** The user's yes. The only value: there is no way to record or send without it. */
enum class Consent(wire: String) {
    OPT_IN("opt_in")
}

/**
 * One position from the device. Only lon, lat and time are ever sent. From an android.location.Location `l`:
 * `PositionFix(l.longitude, l.latitude, if (l.hasAccuracy()) l.accuracy.toDouble() else null, l.time)`.
 *
 * @param accuracy meters, null when not known (kept).
 * @param timeMs milliseconds since 1970 UTC, as Location.getTime().
 */
data class PositionFix(val lon: Double, val lat: Double, val accuracy: Double? = null, val timeMs: Long)

/** Where fixes come from: a LocationListener or FusedLocationProviderClient callback in the app, or a test. The SDK
 * never asks for location permission itself; the app does, and its own permission text says why. */
interface PositionSource {
    /** Call [onFix] for each new position until [stop]. */
    fun start(onFix: (PositionFix) -> Unit)

    /** Stop calling onFix. The client calls it when the trip stops or is cancelled. */
    fun stop()
}

/** A PositionSource the app pushes fixes into, from its location callback:
 * `for (l in result.locations) feed.push(PositionFix(...))`. A fix pushed while no trip is recorded is dropped. */
class PositionFeed : PositionSource {
    override fun start(onFix: (PositionFix) -> Unit)
    override fun stop()

    /** Hands [fix] to the trip being recorded. Dropped when none is. */
    fun push(fix: PositionFix)
}

/** [lon, lat, t]: degrees WGS84 and whole seconds UTC. Nothing else per point. */
data class TracePoint(val lon: Double, val lat: Double, val t: Long)

Types.kt ​

kotlin
/** A WGS84 point. On the wire it is [lon, lat], longitude first. */
data class LonLat(val lon: Double, val lat: Double)

/** National format, as written by the source. */
data class OfficialAddress(
    val country: String,
    val cityOrAimag: String,
    val districtOrSoum: String,
    val khorooOrBag: String?,
    val streetOrMicrodistrict: String?,
    val buildingOrKhashaa: String?,
    val entrance: String?,
    val floor: String?,
    val unit: String?,
    /** Unified postal code from the state register, "13381-0017". */
    val postalCode: String? = null,
    /** The building's own name from the state register, when it has one. */
    val buildingName: String? = null,
)

/** The entrance a building code points to. */
data class Entrance(
    /** door, gate, vehicle_gate or stairwell. */
    val kind: String,
    /** Entrance or gate number as people say it. */
    val letter: String?,
    val floorsServed: List<Int>?,
)

/** The flat or office typed after a code. Unit in the JS and Swift SDKs; DoorUnit here, so kotlin.Unit stays itself. */
data class DoorUnit(
    val number: String,
    val floor: Int?,
    /** flat, office, shop, stall or kiosk. */
    val kind: String,
)

/** A door. Official address first, code after it. No owner, no phone. */
data class Door(
    val officialAddress: OfficialAddress?,
    /** matched, partial or none. */
    val officialStatus: String,
    /** true unless officialStatus is matched. Show "unofficial" when true. */
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    val code: String,
    val naturalMn: String?,
    val naturalEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val entrance: Entrance?,
    val unit: DoorUnit?,
    /** PLAN 5 level, 0 to 4. null means unknown. */
    val confidence: Int?,
    val source: String,
    val instructionsMn: String?,
    val instructionsEn: String?,
    val aliases: List<String>,
    /** The date of the door or unit row behind the code, YYYY-MM-DD. null: not known. Absent before API 0.87.0. */
    val sourceDate: String? = null,
    /** Confirmed driver taps for this door since its point last moved. 0 when none. null from an API before this field. */
    val confirmations: Int? = null,
)

/** One geocode answer. kind: door, business, building, district, khoroo, area, street or landmark. */
data class GeocodeResult(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    /** Building code. null when the place has none. */
    val code: String?,
    val kind: String,
    /** Stable id. Never show it to people as an address. */
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val confidence: Int?,
    val source: String,
    /** Higher is better. Compare only with results of the same layer in one response. */
    val score: Double,
    /** 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 (API 0.30.0). */
    val layer: Int,
    /** The landmark a landmark-relative query named ("Хүннү моллын ард"), for a place found around it. null otherwise. */
    val near: GeocodeNear? = null,
    /** A bus stop: the city bus lines that call at it ("Ч:41"). null for every other place. */
    val lines: List<String>? = null,
    /** A business or landmark: its map category, the one nearby gives ("pharmacy"). null for any other place and when unknown. */
    val category: String? = null,
    /** 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. */
    val sourceDate: 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. */
    val floor: Int? = null,
    /** A company from a building's directory (API 0.90.0): its flat or office number (N тоот). null when not said. */
    val flat: String? = null,
    /** A business: true when its company has a claim the state register confirmed, false when not or pending (API 0.95). null for any other kind. */
    val claimed: Boolean? = null,
    /** A building: apartment, ger_house, commercial, public, industrial, garage or other. null when the building has no type yet, and for any other kind. */
    val buildingType: String? = null,
    /** A business, landmark or street: the khoroo number its point stands in. null when no polygon covers it, and for any other kind. */
    val khoroo: Int? = null,
    /** A business: true when its point lies inside a restricted site, so a client hides the claim link and any edit. null for any other kind. */
    val restricted: Boolean? = null,
    /** A building: true when its whole name is only a kind word (Үсчин, Дэлгүүр), so a client shows it as a kind. null when not checked, and for any other kind. */
    val nameIsKindWord: Boolean? = null,
    /** A building: the OSM category of the one POI in its footprint ("shop=hairdresser"). null when unknown, and for any other kind. */
    val poiCategory: String? = null,
    /** A building: up to 5 company names registered at it. Empty when none; null for any other kind. */
    val poiNames: List<String>? = null,
    /** The official address as one Cyrillic line, postal code last. null when none is matched. */
    val officialLine: String? = null,
    /** A building with an indoor map: venue_id, levels (floor numbers, lowest first) and the units the query names. null for every other row. */
    val indoor: JsonElement? = null,
)

/** The landmark, building or door a near-me query was read against. */
data class GeocodeNear(
    /** landmark, building or door. */
    val kind: String,
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    /** north, east, south or west (within 55 degrees), near or beside (the whole circle). */
    val relation: String,
    /** From the landmark to the place, 150 at most. */
    val metres: Double,
)

/** GET /v1/geocode, or places picked on this device when offline is true. */
data class GeocodeResponse(
    val results: List<GeocodeResult>,
    /** true only when the answer came from this device's picked places, not the API. Not on the wire. */
    val offline: Boolean = false,
    /** Set when the whole query is a unit code, alone ("7QSVX") or spoken ("7QSVX, 12 давхар, 1204 тоот"). null
     * otherwise and offline. */
    val unit: GeocodeUnit? = null,
    /** Units inside a building with an indoor map that the query names (up to 5), each with venue_id, level, name_mn and point. null when none is named. */
    val indoorUnits: List<JsonElement>? = null,
)

/** A unit code found in the query: the unit card as GET /v1/door/{code} answers it, plus the floor and flat said
 * after the code. */
data class GeocodeUnit(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    /** true unless officialStatus is matched. Show "unofficial" when true. */
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    val code: String,
    /** building or khashaa. */
    val kind: String,
    /** The official address as one Cyrillic line, postal code last. null when nothing is known. */
    val officialLine: String?,
    val nameMn: String?,
    val nameEn: String?,
    /** A point inside the footprint. */
    val point: LonLat,
    /** N in "N давхар"; null when not said. */
    val floor: Int?,
    /** N in "N тоот"; null when not said. */
    val flat: String?,
    /** Where the unit row came from. null when unknown. */
    val source: String? = null,
    /** How sure dock is of the building row, 0 to 4; lower for a state-only building. null: unknown. */
    val confidence: Double? = null,
    /** The date of the door or unit row behind the code, YYYY-MM-DD. null: not known. Absent before API 0.87.0. */
    val sourceDate: String? = null,
    /** The building holds an indoor map: its venue id (venue()). null when it holds none. */
    val venueId: String? = null,
    /** District and khoroo the building lies in, only when its address is not matched. Never the official address. null when matched or not known. */
    val areaLine: String? = null,
)

/** One reverse result. */
data class ReverseResult(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    val code: String?,
    val kind: String,
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val confidence: Int?,
    val source: String,
    /** Metres from the query point. 0 means the point is inside. */
    val distanceM: Double,
    /** A bus stop: the city bus lines that call at it ("Ч:41"). null for every other place. */
    val lines: List<String>? = null,
    /** A business or landmark: its map category, the one nearby gives ("pharmacy"). null for any other place and when unknown. */
    val category: String? = null,
    /** 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. */
    val sourceDate: String? = null,
    /** A business: true when its company has a claim the state register confirmed, false when not or pending (API 0.95). null for any other kind. */
    val claimed: Boolean? = null,
    /** A building: apartment, ger_house, commercial, public, industrial, garage or other. null when the building has no type yet, and for any other kind. */
    val buildingType: String? = null,
    /** A business, landmark or street: the khoroo number its point stands in. null when no polygon covers it, and for any other kind. */
    val khoroo: Int? = null,
    /** A business: true when its point lies inside a restricted site, so a client hides the claim link and any edit. null for any other kind. */
    val restricted: Boolean? = null,
    /** A building: true when its whole name is only a kind word (Үсчин, Дэлгүүр), so a client shows it as a kind. null when not checked, and for any other kind. */
    val nameIsKindWord: Boolean? = null,
    /** A building: the OSM category of the one POI in its footprint ("shop=hairdresser"). null when unknown, and for any other kind. */
    val poiCategory: String? = null,
    /** A building: up to 5 company names registered at it. Empty when none; null for any other kind. */
    val poiNames: List<String>? = null,
    /** The official address as one Cyrillic line, postal code last. null when none is matched. */
    val officialLine: String? = null,
    /** A building with an indoor map: venue_id, levels (floor numbers, lowest first) and the units the query names. null for every other row. */
    val indoor: JsonElement? = null,
)

/** A kind geocode can keep (types), as the API's PLACE_KINDS. */
enum class GeocodeType(wire: String) {
    DOOR("door"),
    BUSINESS("business"),
    BUILDING("building"),
    DISTRICT("district"),
    KHOROO("khoroo"),
    AREA("area"),
    STREET("street"),
    LANDMARK("landmark")
}

/** A kind reverse can keep (types), as the API's REVERSE_KINDS: district and khoroo always come with the answer. */
enum class ReverseType(wire: String) {
    DOOR("door"),
    BUSINESS("business"),
    BUILDING("building"),
    LANDMARK("landmark")
}

/** GET /v1/reverse. */
data class ReverseResponse(
    val point: LonLat,
    val districtMn: String?,
    val districtEn: String?,
    /** null when no khoroo polygon covers the point. Never guessed. */
    val khoroo: Int?,
    /** The soum and aimag covering the point (OSM boundaries, API 0.104.0). null when no polygon covers it. */
    val soumMn: String? = null,
    val aimagMn: String? = null,
    val results: List<ReverseResult>,
    /** true when the point itself lies inside a restricted site: results is then empty, and a client hides the claim link and any edit. */
    val restricted: Boolean = false,
)

/** A category nearby takes: the map's place icons, as the API's NEARBY_CATEGORIES. 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). */
enum class NearbyCategory(wire: String) {
    ATM("atm"),
    BANK("bank"),
    BRIDGE("bridge"),
    CAFE("cafe"),
    CAR_WASH("car_wash"),
    CULTURE("culture"),
    EMBASSY("embassy"),
    FUEL("fuel"),
    GOVERNMENT("government"),
    HOSPITAL("hospital"),
    HOTEL("hotel"),
    KINDERGARTEN("kindergarten"),
    MALL("mall"),
    MARKET("market"),
    MONUMENT("monument"),
    OFFICE("office"),
    PARK("park"),
    PHARMACY("pharmacy"),
    RELIGION("religion"),
    RESTAURANT("restaurant"),
    SCHOOL("school"),
    SHOP("shop"),
    SPORT("sport"),
    STATION("station"),
    STOP("stop"),
    UNIVERSITY("university")
}

/** One nearby result. */
data class NearbyResult(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    /** 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, or none. */
    val code: String?,
    val kind: String,
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val confidence: Int?,
    val source: String,
    /** The map category, as the place icon. String, so a new category from the API never fails a decode. */
    val category: String,
    /** The OpenStreetMap opening_hours tag as written; may be old. null when the source has none. */
    val openingHours: String? = null,
    /** Where openingHours came from. null when openingHours is null. */
    val hoursSource: String? = null,
    /** Open now in Ulaanbaatar time, from a plain opening_hours tag. null when unknown: no tag, or not a plain one. */
    val openNow: Boolean? = null,
    /** When the place closes today, HH:MM in Ulaanbaatar time, while openNow is true. null when closed, unknown, or open 24/7. */
    val openUntil: String? = null,
    /** Only when nearby was asked for photo: one openly licensed photo of a landmark or building. null when none or not asked. */
    val photo: PlacePhoto? = null,
    /** Metres from the query point. */
    val distanceM: Double,
    /** A bus stop: the city bus lines that call at it ("Ч:41"). null when the capital's public transport data lists none. */
    val lines: List<String>? = null,
    /** 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. */
    val sourceDate: String? = null,
    /** A registered company from a building's directory: its floor from the state register. null when not said. Absent before API 0.92.1. */
    val floor: Int? = null,
    /** A company's flat or office number (N тоот). null when not said. Absent before API 0.92.1. */
    val flat: String? = null,
    /** A business: true when its company has a claim the state register confirmed, false when not or pending (API 0.95). null for any other kind. */
    val claimed: Boolean? = null,
    /** A business, landmark or street: the khoroo number its point stands in. null when no polygon covers it, and for any other kind. */
    val khoroo: Int? = null,
    /** A business: true when its point lies inside a restricted site, so a client hides the claim link and any edit. null for any other kind. */
    val restricted: Boolean? = null,
    /** The official address as one Cyrillic line, postal code last. null when none is matched. */
    val officialLine: String? = null,
    /** A building with an indoor map: venue_id, levels (floor numbers, lowest first) and the units the query names. null for every other row. */
    val indoor: JsonElement? = null,
)

/** GET /v1/nearby. */
data class NearbyResponse(
    val point: LonLat,
    /** Nearest first, within radius. Empty when none. */
    val results: List<NearbyResult>,
    /** How many more places of these categories lie within radius past limit. 0 when results holds them all. */
    val more: Int,
    /** Only with open=1: how many of the nearest 20 places have no readable hours (openNow null). They are not in results. */
    val hoursUnknown: Int? = null,
)

/** Second-source check of a building footprint. confidence is null when the run gave none: unknown. */
data class FootprintCheck(
    /** confirmed, new or doubtful. */
    val verdict: String,
    val source: String,
    val confidence: Double? = null,
    val checkedAt: String,
)

/** One openly licensed photo of a place. Show credit and license beside it. */
data class PlacePhoto(
    val url: String,
    val credit: String,
    /** CC BY, CC BY-SA, CC0 or public domain. */
    val license: String,
    val source: String,
)

/** Where to point the map from above when a place has no photo. */
data class MapFromAbove(
    val lon: Double,
    val lat: Double,
    val zoom: Int,
)

/** Ids of what a door stands in. Ids only, never shown to people as an address. null when not known. */
data class Parents(
    /** The door of a business. null for a door. */
    val door: String?,
    val building: String?,
    /** Khashaa (plot). */
    val plot: String?,
    val area: String?,
    val street: String?,
    /** null when no khoroo polygon covers the door. Never guessed. */
    val khoroo: String?,
    val district: String?,
)

/** GET /v1/address/{id}: one place by the id geocode, reverse or changes gave. */
data class Address(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    val code: String?,
    val kind: String,
    val id: String,
    val nameMn: String?,
    val nameEn: String?,
    val longMn: String?,
    val longEn: String?,
    val point: LonLat,
    val confidence: Int?,
    val source: String,
    /** Doors and businesses; a khoroo carries its district only; null for every other kind. */
    val parents: Parents?,
    /** A bus stop: the city bus lines that call at it ("Ч:41"). null for every other place. */
    val lines: List<String>? = null,
    /** A business or landmark: its map category, the one nearby gives ("pharmacy"). null for any other place and when unknown. */
    val category: String? = null,
    /** 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. */
    val sourceDate: 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. */
    val floor: Int? = null,
    /** A company from a building's directory (API 0.90.0): its flat or office number (N тоот). null when not said. */
    val 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. */
    val outline: JsonElement? = null,
    /** A business: true when its company has a claim the state register confirmed, false when not or pending (API 0.95). null for any other kind. */
    val claimed: Boolean? = null,
    /** A district, khoroo or area: current buildings and doors inside its boundary, nothing inside a restricted site. null for every other kind and where the register has no polygon: unknown. 0 is a real zero. */
    val counts: JsonElement? = null,
    /** A business, landmark or street: the khoroo number its point stands in. null when no polygon covers it, and for any other kind. */
    val khoroo: Int? = null,
    /** A business: true when its point lies inside a restricted site, so a client hides the claim link and any edit. null for any other kind. */
    val restricted: Boolean? = null,
    /** A building: whether its footprint is backed by a second source. null for other kinds and for a building not yet checked. */
    val footprintCheck: FootprintCheck? = null,
    /** A landmark, business or street: the district its point stands in, in Mongolian. null when no polygon covers it, and for any other kind. */
    val districtMn: String? = null,
    /** A landmark or building: one openly licensed photo within 30 m. null when there is none. */
    val photo: PlacePhoto? = 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. */
    val mapFromAbove: MapFromAbove? = null,
    /** The official address as one Cyrillic line, postal code last. null when none is matched. */
    val officialLine: String? = null,
    /** A building with an indoor map: venue_id, levels (floor numbers, lowest first) and the units the query names. null for every other row. */
    val indoor: JsonElement? = null,
)

/** One place changed, in changes(). */
data class Change(
    /** The id place() takes. */
    val id: String,
    val kind: String,
    /** current: fetch it again with place(). retired: drop it, the id is never reused. hidden: drop it, it may come back. */
    val state: String,
    /** Last change, UTC, microseconds. */
    val changedAt: String,
)

/** GET /v1/changes. Window [since, until). */
data class ChangesResponse(
    val since: String,
    /** The last index refresh. When next is null, poll again with since = until. */
    val until: String,
    /** Oldest first. One row per place, its last change. */
    val changes: List<Change>,
    /** More in this window: pass as cursor. null: window done. */
    val next: String?,
)

/** How the route is travelled. */
enum class RouteMode(wire: String) {
    DRIVING("driving"),
    WALKING("walking"),
    CYCLING("cycling")
}

/** The door at one end of a route. Show "(unofficial)" when unofficial is true. */
data class RouteDoor(
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    val code: String,
    val naturalMn: String?,
    val naturalEn: String?,
    val entrance: Entrance?,
    val confidence: Int?,
)

/**
 * The entrance at one end of a route, when that end was sent as an entrance id. Show "(unofficial)" when unofficial
 * is true; confidence 0 with source generated was placed by rule (таамаг).
 */
data class RouteEntrance(
    val id: String,
    val building: String,
    /** null: no door minted for this entrance yet. */
    val code: String?,
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    val unofficial: Boolean,
    /** door, gate, vehicle_gate or stairwell. */
    val kind: String,
    val letter: String?,
    val confidence: Int?,
    val source: String,
)

/** One end of a route. */
data class RouteEnd(
    /** What was asked for: the entrance when a code was given. */
    val point: LonLat,
    /** Where the line starts or ends, on the road network. */
    val snapped: LonLat,
    /** Straight metres from snapped to point. Walked, never drawn as road. */
    val gapM: Double,
    val door: RouteDoor?,
    /** The entrance, when this end was sent as an entrance id. Absent from older APIs. */
    val entrance: RouteEntrance? = null,
    /** The address unit this end was sent as, by its code; null for any other end. */
    val unit: RouteUnit? = null,
    /** 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. */
    val sourceDate: String? = null,
)

/** The address unit (building or khashaa) a route end was given as, by its code. The route ends at its street side. */
data class RouteUnit(
    val code: String,
    /** building or khashaa. */
    val kind: String,
    val officialAddress: OfficialAddress?,
    val officialStatus: String,
    /** true unless officialStatus is matched. Show "unofficial" when true. */
    val 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. */
    val streetSide: Boolean,
)

/** A GeoJSON LineString: type "LineString" and [lon, lat] points. */
data class LineString(val type: String, val coordinates: List<LonLat>)

/** The road closures a route avoided (GET /v1/closures, active on asOf). */
data class RouteClosures(
    /** The route day, YYYY-MM-DD, Ulaanbaatar. */
    val asOf: String,
    /** Closure ids, in id order. Each was kept out of the route, whether or not it came near. Show them with it. */
    val avoided: List<String>,
)

/** Driving time with all partners' measured moving speed per road (API 0.48.0). */
data class RouteMeasured(
    /** YYYY-MM: the latest month GET /v1/traces/roads publishes. */
    val month: String,
    /** The hour band of departAt whose speeds were read (API 0.50.0), a RoadBand wire name: "am_peak", "pm_peak",
     * "off_peak". null without departAt (all hours) or from an API before 0.50.0. */
    val band: String? = null,
    /** Meters of the line on roads with a published speed that month. 0: none, and durationS is the free-flow one. */
    val roadsM: Double,
    /** Seconds with those roads at their measured moving speed, the rest at the router's. A month average of the band. */
    val durationS: Double,
)

/** GET /v1/route. durationS is free-flow; measured has partners' speeds. */
data class Route(
    val mode: String,
    val distanceM: Double,
    /** Seconds at the router's free-flow speeds. */
    val durationS: Double,
    val geometry: LineString,
    val from: RouteEnd,
    val to: RouteEnd,
    val closures: RouteClosures,
    val source: String,
    /** Driving only; null when walking or cycling, when speeds could not be read, or from an API before 0.48.0. */
    val measured: RouteMeasured? = null,
    /** One per hop, in order: from to via[0], ..., the last to to; one without via. null from an API before 0.67.0. */
    val legs: List<TripLeg>? = null,
    /** The via ends in the order sent, snapped where the line passes them; empty without. null before API 0.67.0. */
    val via: List<RouteEnd>? = null,
    /** Driving: seconds with a typical load by road class, hour and weekday; null when walking, cycling or unreadable (API 0.97.19). */
    val expectedDurationS: Int? = null,
    /** Where that load comes from: model (a table), probes (partners' phones, not yet). */
    val trafficSource: String? = null,
    /** The label to show beside expectedDurationS. */
    val trafficNote: String? = null,
)

/** 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. */
data class Matrix(
    val mode: String,
    /** Seconds at the router's free-flow speeds, as durationS in a route. */
    val durationsS: List<List<Double?>>,
    /** Meters from from[i].snapped to to[j].snapped; null where durationsS is null. */
    val distancesM: List<List<Double?>>,
    /** The sources in the order asked, each as a route end. */
    val from: List<RouteEnd>,
    /** The targets in the order asked, each as a route end. */
    val to: List<RouteEnd>,
    val closures: RouteClosures,
    val source: String,
)

/** 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. */
data class Isochrone(
    /** "FeatureCollection". */
    val type: String,
    val features: List<IsochroneFeature>,
    val mode: String,
    /** Where the areas are reached from, as a route end. */
    val from: RouteEnd,
    val closures: RouteClosures,
    val source: String,
)

/** One isochrone contour: the area reached within `properties.contour` minutes. */
data class IsochroneFeature(
    /** "Feature". */
    val type: String,
    val properties: IsochroneProperties,
    val geometry: IsochroneGeometry,
)

/** An isochrone contour's fields, in IsochroneFeature.properties. */
data class IsochroneProperties(
    /** Minutes: the area reached from from within this time. */
    val contour: Int,
    /** Always "time": minutes, as Mapbox's contours_minutes. */
    val metric: String,
)

/** An isochrone's area: a Polygon, or a MultiPolygon where the area is in pieces. Every ring closed, [lon, lat]. */
sealed class IsochroneGeometry {
    /** Every piece of the area, each as its rings (outer first, then holes), one for a Polygon: fill them all the same. */
    abstract val polygons: List<List<List<LonLat>>>

    /** The area in one piece: its rings. */
    data class Polygon(val rings: List<List<LonLat>>) : IsochroneGeometry() {
        override val polygons: List<List<List<LonLat>>>
    }

    /** The area in pieces. */
    data class MultiPolygon(override val polygons: List<List<List<LonLat>>>) : IsochroneGeometry()
}

/** 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. */
data class OptimizedTrip(
    val mode: String,
    /** true: the trip goes back to the first stop. false: it ends at the last stop sent. */
    val roundtrip: Boolean,
    val trips: List<Trip>,
    val waypoints: List<TripWaypoint>,
    val closures: RouteClosures,
    val source: String,
)

/** The whole trip in the order driven, snapped stop to snapped stop. */
data class Trip(
    val distanceM: Double,
    /** Seconds on the road at the router's free-flow speeds. No live traffic, no measured speeds, no time at the stops. */
    val durationS: Double,
    val geometry: LineString,
    /** legs[k] goes from the stop with waypointIndex k to the one with k + 1; on a round trip the last goes back to
     * the first. */
    val legs: List<TripLeg>,
)

/** 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). */
data class TripLeg(
    /** Meters from one stop to the next along the road network. */
    val distanceM: Double,
    /** Seconds from one stop to the next at the router's free-flow speeds. */
    val durationS: Double,
)

/** One stop as sent, a route end with its place in the trip. */
data class TripWaypoint(
    /** What was asked for: the entrance when a code was given. */
    val point: LonLat,
    /** Where the line starts or ends, on the road network. */
    val snapped: LonLat,
    /** Straight metres from snapped to point. Walked, never drawn as road. */
    val gapM: Double,
    val door: RouteDoor?,
    /** Where this stop comes in the trip: 0 is the first, always the first sent. */
    val waypointIndex: Int,
    val entrance: RouteEntrance? = null,
    /** The address unit this stop was sent as, by its code; null for any other stop. */
    val unit: RouteUnit? = null,
    /** The date of the door, unit or entrance behind this stop, YYYY-MM-DD; null for a bare point. Absent before API 0.87.0. */
    val sourceDate: String? = null,
)

/** A tile source tilequery reads: the building, entrance and place layers of the map. */
enum class TilequerySource(wire: String) {
    BUILDING("building"),
    ENTRANCE("entrance"),
    PLACE("place")
}

/** A feature's geometry type in its tile, as Mapbox's tilequery.geometry; keeps one type in a tilequery. */
enum class TilequeryGeometry(wire: String) {
    POLYGON("polygon"),
    LINESTRING("linestring"),
    POINT("point")
}

/** 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. */
data class Tilequery(
    /** "FeatureCollection". */
    val type: String,
    val features: List<TilequeryFeature>,
)

/** One building or entrance at or near the point: a Point, the part of it nearest the point sent. */
data class TilequeryFeature(
    /** "Feature". */
    val type: String,
    val geometry: TilequeryPoint,
    val properties: TilequeryProperties,
)

/** A tilequery feature's place: the point sent when it is inside. The 7th decimal is a mark for your organization. */
data class TilequeryPoint(
    /** "Point". */
    val type: String,
    val coordinates: LonLat,
)

/** 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. */
data class TilequeryProperties(
    /** The building or entrance id, as in its tile. Never an address. */
    val id: String,
    /** Building only. */
    val nameMn: String? = null,
    /** Building only. */
    val nameEn: String? = null,
    /** Building only: apartment, ger_house, commercial, public, industrial, garage or other. */
    val type: String? = null,
    /** Building only: planned, under_construction or built. */
    val status: String? = null,
    /** Building only. Metres. */
    val heightM: Double? = null,
    /** Building only: where heightM came from. osm:height a tag; levels levels x 3.1 m; default:* a type default;
     * estimated:* footprint shape and size; unknown none. */
    val heightSource: String? = null,
    /** Building only: 3 a tag or editor, 2 levels, 1 type default, 0 estimated. 0 and 1 are estimates, never
     * measurements. null: no height. */
    val heightConfidence: Int? = null,
    /** Building only. */
    val levels: Int? = null,
    /** Building only: official building number from the state register. null: no matched or partial official form. */
    val buildingNumber: String? = null,
    /** Building only: the building's name when it is a name, not a purpose. null: no name or no official form. */
    val buildingName: String? = null,
    /** Entrance only: door, gate, vehicle_gate or stairwell. */
    val kind: String? = null,
    /** Entrance only. */
    val letter: String? = null,
    /** Entrance only: its building. null: none on record. */
    val buildingId: String? = null,
    /** 0 to 5 (PLAN.md 5). null: not rated yet. */
    val confidence: Int?,
    val source: String,
    /** Place only: the map category (bank, pharmacy, stop, religion, etc.). */
    val cat: String? = null,
    /** Place only: the map rank (higher shows over lower in collisions). */
    val rank: Double? = null,
    val tilequery: TilequeryInfo,
    /** A building: whether its footprint is backed by a second source. null for other kinds and for a building not yet checked. */
    val footprintCheck: FootprintCheck? = null,
)

/** How a tilequery feature lies from the point sent. */
data class TilequeryInfo(
    /** Metres from the point sent to the nearest part of the feature, to 0.1 m. 0: the point is inside it. */
    val distance: Double,
    /** polygon, linestring or point. */
    val geometry: String,
    /** The tile source and layer the feature is in: building or entrance. */
    val layer: String,
)

/** 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. */
data class Match(
    val mode: String,
    val matchings: List<Matching>,
    val tracepoints: List<MatchTracepoint?>,
    val source: String,
)

/** One unbroken piece of a matched trace: the roads it went along, in its order. */
data class Matching(
    val distanceM: Double,
    /** Seconds at the router's free-flow speed on each road, no turn costs; not the time the trace took. */
    val durationS: Double,
    val geometry: LineString,
)

/** Where one point sent lies on the matched line. */
data class MatchTracepoint(
    /** The matching this point lies on, an index into matchings. */
    val matchingsIndex: Int,
    /** The point on the matched line. */
    val location: LonLat,
    /** Straight-line meters from the point sent to location. */
    val gapM: Double,
)

/** POST /v1/door/report. A person reviews it; door data does not change until then. */
sealed class DoorReport {
    /** Words for the reviewer, or null. */
    abstract val note: String?

    /** The door with [code] is at [point] instead. */
    data class WrongLocation(val code: String, val point: LonLat, override val note: String? = null) : DoorReport()

    /** No door is where [code] points. */
    data class Missing(val code: String, override val note: String? = null) : DoorReport()

    /** The door with [code] is the same door as the one with [duplicateOf]. */
    data class Duplicate(val code: String, val duplicateOf: String, override val note: String? = null) : DoorReport()

    /** A door at [point] that has no code. */
    data class Unmapped(val point: LonLat, override val note: String? = null) : DoorReport()
}

/** The answer to POST /v1/door/report. */
data class DoorReportReceipt(
    val id: String,
    /** queued or already_queued. */
    val status: String,
    val kind: String,
    val code: String?,
    /** door, building, plot, or null. */
    val target: String?,
    val movedM: Double?,
    val receivedAt: String,
    val next: String,
)

/** POST /v1/confirm body. A driver on site says a door is there, elsewhere or gone. */
data class Confirmation(
    val target: Target,
    val result: Result,
    /** Phone GPS on site. A check only, never the placed point: within 40 m of the tap. */
    val device: LonLat,
    /** Your own opaque id for the driver (A-Z a-z 0-9 . _ : -, up to 64). Never a phone number, e-mail or name. */
    val driver: String? = null,
    val note: String? = null,
    /** When the driver was on site, for a confirmation queued offline. At most 7 days ago. */
    val observedAt: Date? = null,
) {
    /** The door by code, or by the entrance id in the entrance tile layer for entrances with no code yet. */
    sealed class Target {
        /** A door by its code. */
        data class Code(val code: String) : Target()

        /** An entrance with no code yet, by its id in the entrance tile layer. */
        data class Entrance(val id: String) : Target()
    }

    /** Confirmed and WrongLocation carry the point the driver tapped on the map: the placed point. */
    sealed class Result {
        /** The door is where the map has it. */
        data class Confirmed(val tap: LonLat) : Result()

        /** The door is elsewhere: [tap] is where. */
        data class WrongLocation(val tap: LonLat) : Result()

        /** No door there. No tap: there is no door to tap. */
        object Missing : Result() {
            override fun toString(): String
        }
    }
}

/** POST /v1/confirm. Map data does not change at once. */
data class ConfirmReceipt(
    /** Same id for a repeat. */
    val id: String,
    /** recorded, or already_recorded: the same driver (or your org, with no driver) said the same in the last 24 h. */
    val status: String,
    val result: String,
    /** door, entrance or unit (the code named a building or khashaa: an arrival). */
    val target: String,
    val code: String?,
    /** The entrance confirmed, also for a door code. null for a unit code. */
    val entrance: String?,
    /** Metres from the tap to the entrance on the map. null for missing. */
    val movedM: Double?,
    /** Metres from the tap (missing: the entrance) to the phone GPS. */
    val gpsM: Double,
    val observedAt: String?,
    val receivedAt: 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. */
    val next: String,
)

/** Confirmations and door reports by result. duplicate and unmapped come from door reports. */
data class ConfirmCounts(
    val confirmed: Int,
    val wrongLocation: Int,
    val missing: Int,
    val duplicate: Int,
    /** A real door with no code yet. */
    val unmapped: Int,
)

/** One Ulaanbaatar day the drivers were on site (observed_at, else received). The counts as in [ConfirmCounts]. */
data class ConfirmDay(
    /** YYYY-MM-DD. */
    val day: String,
    /** Different drivers that day, by your driver ids. Rows with no driver id are not counted here. */
    val drivers: Int,
    val confirmed: Int,
    val wrongLocation: Int,
    val missing: Int,
    val duplicate: Int,
    val unmapped: Int,
)

/** A failure point: a door or entrance your drivers said is wrong, missing or a duplicate. */
data class ConfirmFlagged(
    /** null when your drivers named the entrance, not a code. */
    val code: String?,
    /** The entrance id, as in the entrance tile layer. */
    val entrance: String,
    /** true when the door or entrance has since been retired. */
    val retired: Boolean,
    val wrongLocation: Int,
    val missing: Int,
    val duplicate: Int,
    /** The newest of them, on site. */
    val lastAt: String,
)

/** GET /v1/confirm/own: your organization's rows only. */
data class OwnConfirmations(
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** false while confirmations queued offline can still join; true from 8 days after the month ends. */
    val closed: Boolean,
    val totals: ConfirmCounts,
    /** Different drivers in the month, by your driver ids. Never the ids. */
    val drivers: Int,
    /** Days with at least one row, oldest first. */
    val days: List<ConfirmDay>,
    /** Most first, at most 100. None inside a restricted site. */
    val flagged: List<ConfirmFlagged>,
)

/** Daily caps; null is no cap. */
data class UsageCaps(val dailyQuota: Int?, val tileDailyQuota: Int?, val doorMissCap: Int?)

/** What a free org gets on the partner plan. Prices are not published yet. */
data class UsageUpgrade(
    val plan: String,
    val caps: UsageCaps,
    val rateLimit: Int,
    val tileRateLimit: Int,
    val addedScopes: List<String>,
)

/** The key that made the usage call. */
data class UsageKey(
    /** First 11 characters of the key that made this call. */
    val prefix: String?,
    val scopes: List<String>,
    val rateLimit: Int,
    val tileRateLimit: Int,
)

/** One day in usage(). */
data class UsageDay(
    /** Ulaanbaatar day, YYYY-MM-DD. */
    val day: String,
    val calls: Int,
    val tiles: Int,
    val doorMisses: Int,
    val keys: Int,
)

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

/** GET /v1/usage: the calling key's organization. Caps are null when there is no cap. */
data class UsageResponse(
    val org: String,
    /** free: made by POST /v1/signup. custom: a partner org. */
    val plan: String,
    /** The partner plan for a free org, null for a partner org. */
    val upgrade: UsageUpgrade?,
    val timezone: String,
    /** Unix seconds of the next daily reset. */
    val reset: Long,
    val key: UsageKey,
    val caps: UsageCaps,
    /** Newest first. */
    val days: List<UsageDay>,
    /** 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`. null from an API before 0.51.0. */
    val features: List<String>? = null,
    /** Every route the org called over the same days, most calls first, then A to Z (API 0.62.0). null from an API
     * before 0.62.0. */
    val endpoints: List<UsageEndpoint>? = null,
)

/** POST /v1/telemetry/search. */
data class SearchTelemetryReceipt(
    val stored: Int,
    /** Text that was only a phone or an e-mail address, or a pick not in the index. */
    val dropped: Int,
)

/** POST /v1/traces. */
data class TraceReceipt(
    /** Keep it to fetch or delete the trip. null: nothing was stored (every point was inside a restricted site). */
    val id: String?,
    val points: Int,
    /** Points left out: inside a restricted site. */
    val restricted: Int,
    /** Your organization sent this trip before; id is the first upload. */
    val duplicate: Boolean,
    /** Raw points are erased after this day, YYYY-MM-DD. */
    val purgeAfter: String?,
)

/** A stored trip's line. Each coordinate is [lon, lat, t]. */
data class TraceLine(val type: String, val coordinates: List<List<Double>>)

/** A stored trip's fields, in Trace.properties. */
data class TraceProperties(
    val receivedDay: String,
    val purgeAfter: String,
    val points: Int,
    val restricted: Int,
    val deletedAt: String?,
    /** partner or retention, null while the points are kept. */
    val deletedReason: String?,
)

/** GET /v1/traces/{id}: your trip as stored, GeoJSON. geometry is null once deleted or past purge_after. */
data class Trace(val type: String, val id: String, val geometry: TraceLine?, val properties: TraceProperties)

/** DELETE /v1/traces/{id}. deleted is true also when the points were gone before. */
data class TraceDeleted(val id: String, val deleted: Boolean)

/** west, south, east, north in degrees. */
data class BBox(val west: Double, val south: Double, val east: Double, val north: Double)

/** A cell's square, GeoJSON Polygon: one closed ring of [lon, lat]. */
data class TraceCellPolygon(val type: String, val coordinates: List<List<List<Double>>>)

/** A cell's fields, in TraceCell.properties. */
data class TraceCellProperties(
    /** Distinct trips through the cell that month. */
    val trips: Int,
)

/** One 500 m cell of the fixed grid in a month. id is "x/y", the same cell every month. */
data class TraceCell(val type: String, val id: String, val geometry: TraceCellPolygon, val properties: TraceCellProperties)

/** GET /v1/traces/cells: all partners together, only cells that meet [rule]. No cell means too few, not zero. */
data class TraceCells(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** Keys as the API writes them (min_orgs, cell, period). */
    val rule: JsonObject,
    val features: List<TraceCell>,
)

/** A cell's fields, in TraceSpeed.properties. */
data class TraceSpeedProperties(
    /** Counted meters over counted seconds of all partners' trips in the cell, to 0.1. No trip count. */
    val kmH: Double,
)

/** One 500 m cell of the trip cells grid with all partners' moving speed that month. id is "x/y", as TraceCell. */
data class TraceSpeed(val type: String, val id: String, val geometry: TraceCellPolygon, val properties: TraceSpeedProperties)

/** GET /v1/traces/speeds: all partners together, only cells that meet [rule]. No cell means too few trips, not slow. */
data class TraceSpeeds(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** The group rule each cell passed, and how a segment and km_h are measured, keys as the API writes them. */
    val rule: JsonObject,
    /** Cells north to south, then west to east. */
    val features: List<TraceSpeed>,
)

/** A road's line: dock's current road with that OSM id, a LineString, or a MultiLineString when it is in pieces. */
sealed class TraceRoadGeometry {
    /** Every piece of the line, one for a LineString: draw them all the same way. */
    abstract val lines: List<List<LonLat>>

    /** The road in one piece. */
    data class LineString(val points: List<LonLat>) : TraceRoadGeometry() {
        override val lines: List<List<LonLat>>
    }

    /** The road in pieces. */
    data class MultiLineString(override val lines: List<List<LonLat>>) : TraceRoadGeometry()
}

/** A road's fields, in TraceRoad.properties. */
data class TraceRoadProperties(
    /** Counted meters over counted seconds of all partners' trips matched to the way, to 0.1. No trip count. */
    val kmH: Double,
    /** The road's names and OSM highway class, null when unknown. */
    val nameMn: String?,
    val nameEn: String?,
    val highway: String?,
)

/** 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. */
data class TraceRoad(val type: String, val id: String, val geometry: TraceRoadGeometry, val properties: TraceRoadProperties)

/** 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. */
enum class RoadBand(wire: String) {
    AM_PEAK("am_peak"),
    PM_PEAK("pm_peak"),
    OFF_PEAK("off_peak")
}

/** GET /v1/traces/roads: all partners together, only roads that meet [rule]. No road means too few trips, not slow. */
data class TraceRoads(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** The group rule each road passed, and how a trip is matched and km_h measured, keys as the API writes them. */
    val rule: JsonObject,
    /** Roads by OSM way id, smallest first. */
    val features: List<TraceRoad>,
)

/** GET /v1/traces/cells/own: your organization's trips only, every cell with one of them. */
data class OwnTraceCells(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** false while trips received now still count; true from 14 days after the month ends. */
    val closed: Boolean,
    val rule: JsonObject,
    val features: List<TraceCell>,
)

/** Trips per whole arrival minute. */
data class ArrivalMinute(
    /** Whole minutes; 60 is 60 or more. */
    val minutes: Int,
    val trips: Int,
)

/** A cell's fields, in ArrivalCell.properties. */
data class ArrivalCellProperties(
    /** Your trips that ended in the cell that month. */
    val trips: Int,
    /** Of them, arrivals of 5 minutes or more. */
    val slow: Int,
    /** Median arrival minutes in the cell, nearest rank. */
    val median: Int,
)

/** A 500 m cell of the trip cells grid where your trips ended. id is "x/y", the same cell every month. */
data class ArrivalCell(val type: String, val id: String, val geometry: TraceCellPolygon, val properties: ArrivalCellProperties)

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

/** Trips that started in one Ulaanbaatar weekday and hour: yours in OwnHours, all partners' in AllHours. */
data class HourSlot(
    /** ISO weekday in Ulaanbaatar, 1 Monday to 7 Sunday. */
    val weekday: Int,
    /** Hour in Ulaanbaatar, 0 to 23. */
    val hour: Int,
    val trips: Int,
)

/** GET /v1/traces/hours/own: when your trips start, by weekday and hour. Your organization's trips only. */
data class OwnHours(
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** false while trips received now still count; true from 14 days after the month ends. */
    val closed: Boolean,
    /** How the weekday and hour are read, keys as the API writes them. */
    val rule: JsonObject,
    /** Your trips that month, each once, at the weekday and hour of its first stored point. */
    val trips: Int,
    /** 7 counts, Monday first. */
    val byWeekday: List<Int>,
    /** 24 counts, hour 0 first. */
    val byHour: List<Int>,
    /** Weekday and hour with trips, weekday then hour; slots with none left out. */
    val slots: List<HourSlot>,
)

/** GET /v1/traces/hours: when trips start, by weekday and hour, all partners together. A published month only. */
data class AllHours(
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** The group rule each slot passed, and how the weekday and hour are read, keys as the API writes them. */
    val rule: JsonObject,
    /** Weekday and hour that passed the rule, weekday then hour. A slot left out had too few trips, not zero. No total. */
    val slots: List<HourSlot>,
)

/** A khoroo's area, GeoJSON MultiPolygon: polygons of closed rings of [lon, lat]. */
data class KhorooPolygon(val type: String, val coordinates: List<List<List<List<Double>>>>)

/** Trips that ended in one khoroo in one Ulaanbaatar hour, all partners together. */
data class KhorooHour(
    /** Hour in Ulaanbaatar of the trip end, 0 to 23. */
    val hour: Int,
    val trips: Int,
)

/** A khoroo's fields, in TraceKhoroo.properties. */
data class TraceKhorooProperties(
    val districtMn: String,
    val districtEn: String?,
    /** Khoroo number in its district. */
    val khoroo: Int,
    /** Hours that passed the rule, from 0. An hour left out had too few trips, not zero. No khoroo total. */
    val hours: List<KhorooHour>,
)

/** A current khoroo polygon with the hours of arrival that passed the rule. id is the polygon's, never an address. */
data class TraceKhoroo(val type: String, val id: String, val geometry: KhorooPolygon, val properties: TraceKhorooProperties)

/** GET /v1/traces/khoroos: where and when trips end, per khoroo and hour, all partners together. A published month only. */
data class TraceKhoroos(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** The group rule each khoroo and hour passed, and how they are read, keys as the API writes them. */
    val rule: JsonObject,
    /** Khoroos with at least one hour shown, by district then number. */
    val features: List<TraceKhoroo>,
)

/** A khoroo's fields, in OwnKhoroo.properties. */
data class OwnKhorooProperties(
    val districtMn: String,
    val districtEn: String?,
    /** Khoroo number in its district. */
    val khoroo: Int,
    /** Your trips that ended in this khoroo that month. */
    val trips: Int,
    /** Your trips per Ulaanbaatar hour of arrival, from 0. Hours with none left out. */
    val hours: List<KhorooHour>,
)

/** A current khoroo polygon with your trips that ended in it. id is the polygon's, never an address. */
data class OwnKhoroo(val type: String, val id: String, val geometry: KhorooPolygon, val properties: OwnKhorooProperties)

/** GET /v1/traces/khoroos/own: where and when your trips end, per khoroo and hour. Your organization only, no group rule. */
data class OwnKhoroos(
    val type: String,
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** false while trips received now still count; true from 14 days after the month ends. */
    val closed: Boolean,
    /** How the khoroo and hour are read, keys as the API writes them. */
    val rule: JsonObject,
    /** Your trips that month. */
    val trips: Int,
    /** Your trips that month ending in no current khoroo polygon. trips is noKhoroo plus every khoroo's trips. */
    val noKhoroo: Int,
    /** Khoroos with at least one of your trips, by district then number. */
    val features: List<OwnKhoroo>,
)

/** Your trips that month ending at one kind of building. */
data class OwnKind(
    /** The building type: apartment, ger_house, commercial, public, industrial, garage, other; unknown is a building
     * with no type yet, none no building within 30 m. */
    val kind: String,
    val trips: Int,
)

/** GET /v1/traces/kinds/own: what kind of building your trips end at, per month. Your organization only, no group rule. */
data class OwnKinds(
    /** YYYY-MM, Ulaanbaatar. */
    val month: String,
    /** false while trips received now still count; true from 14 days after the month ends. */
    val closed: Boolean,
    /** How the building and its kind are read, keys as the API writes them. */
    val rule: JsonObject,
    /** Your trips that month, the sum of every kind's trips. */
    val trips: Int,
    /** Kinds with at least one of your trips, most trips first, then by name. */
    val kinds: List<OwnKind>,
)

/** One entrance in entrancesNear(): nearest first, each with its building and distance. source generated +
 * confidence 0: show as таамаг. No door minted yet: code null. */
data class NearEntrance(
    val officialAddress: OfficialAddress?,
    /** matched, partial or none. */
    val officialStatus: String,
    /** true unless officialStatus is matched. Show "unofficial" when true. */
    val unofficial: Boolean,
    val officialSource: String?,
    val officialDate: String?,
    /** null: no door minted for this entrance yet. */
    val code: String?,
    /** Never show as an address. */
    val id: String,
    /** door, gate, vehicle_gate or stairwell. */
    val kind: String,
    /** Entrance or gate number as people say it. */
    val letter: String?,
    val floorsServed: List<Int>?,
    val point: LonLat,
    /** PLAN 5 level; 0 with source generated: таамаг. */
    val confidence: Int?,
    val source: String,
    /** The building id: pass to place(id) or entrances(). */
    val building: String,
    val distanceM: Double,
    /** The date of this entrance's current version, YYYY-MM-DD. null: not known. Absent before API 0.87.1. */
    val sourceDate: String? = null,
)

/** GET /v1/entrances/near. */
data class NearEntrances(
    val point: LonLat,
    /** Nearest first, within radius. Empty when no entrance is that close. */
    val results: List<NearEntrance>,
)

/** One organization in a building's directory. Only organizations matched to an official registration are listed. */
data class DirectoryEntry(
    val name: String,
    /** Floor (давхар). null when the register does not say. */
    val floor: Int?,
    /** Unit (тоот). null when unknown. */
    val unit: String?,
    val category: String?,
    val source: String,
    val confidence: Double?,
    /** Date this version of the entry was recorded. */
    val sourceDate: String,
)

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

Venue.kt ​

kotlin
/** One floor of a venue. [ordinal] is the floor as people say it: 0 ground, 1 first, -1 basement. */
data class IndoorLevel(
    val ordinal: Int, val shortNameMn: String, val nameMn: String? = null, val outdoor: Boolean, val source: String,
    val confidence: Double? = null,
)

/** Where a venue stands: the district and khoroo covering it, and soum and aimag from the OSM admin polygons. */
data class VenueAdmin(
    val districtMn: String? = null, val districtEn: String? = null, val khoroo: Int? = null, val soumMn: String? = null,
    val aimagMn: String? = null,
)

/** GET /v1/venue/{id}: a building with an indoor map, its outline (GeoJSON Polygon) and its current floors, lowest first. */
data class Venue(
    val id: String, val nameMn: String, val nameEn: String? = null, val category: String, val source: String,
    val confidence: Double? = null,
    /** The building code of the real-map building the venue stands in; null until it has one. */
    val buildingCode: String? = null,
    val admin: VenueAdmin? = null, val outline: JsonElement, val levels: List<IndoorLevel>,
)

/** One unit, opening or amenity on a floor. `properties["feature_type"]` says which. */
data class LevelFeature(val type: String, val id: String, val geometry: JsonElement, val properties: JsonElement)

/** GET /v1/venue/{id}/level/{n}: one floor as GeoJSON. */
data class VenueLevel(val type: String, val venueId: String, val level: Int, val features: List<LevelFeature>)

/** GET /v1/venue/{id}/spot: what a printed plate for one unit says. [link] is what the plate's QR holds. */
data class VenueSpot(
    val venueId: String, val venueNameMn: String, val unitId: String, val level: Int, val levelNameMn: String,
    val nameMn: String? = null, val number: String? = null, val label: String, val point: LonLat, val link: String,
    val source: String, val confidence: Double? = null,
)

/** One step of a walk inside a venue, in Mongolian, with the floor it is on. [at] is where the step begins. */
data class IndoorStep(val floor: Int, val text: String, val distanceM: Double, val at: LonLat)

/** One floor of a walk: its line and how it ends: stairs, escalator, elevator or arrive. [toFloor] is the floor a
 * connector reaches, null on arrival. */
data class IndoorLeg(
    val floor: Int, val distanceM: Double, val geometry: LineString, val endsWith: String, val toFloor: Int? = 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. */
data class VenueRoute(
    val venueId: String, val distanceM: Double, val durationS: Double, val legs: List<IndoorLeg>,
    val steps: List<IndoorStep>, val to: JsonElement,
)

/** The street entrance a road route ends at. */
data class VenueEntrance(val level: Int, val point: LonLat, val coreEntranceId: String? = null)

/** One road step, in both languages. */
data class VenueRoadStep(val mn: String, val en: String, val distanceM: Double, val at: LonLat)

/** The road part of venue directions: the line to the street entrance. */
data class VenueRoad(
    val mode: String, val distanceM: Double, val durationS: Double, val geometry: LineString,
    val steps: List<VenueRoadStep>, val entrance: VenueEntrance,
)

/** GET /v1/venue/{id}/directions: the road to the street entrance, then the walk inside. Distance and duration are
 * road plus walk. */
data class VenueDirections(
    val venueId: String, val distanceM: Double, val durationS: Double, val road: VenueRoad, val indoor: VenueRoute,
)

/** A building with an indoor map: outline and floors. [venueId] is the `indoor.venueId` a geocode row carries.
 * Needs door:read. 404 not_found for an unknown id. */
suspend fun Client.venue(venueId: String): 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. */
suspend fun Client.venueLevel(venueId: String, level: Int): VenueLevel

/** What a printed plate for one unit says: venue, floor, 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. */
suspend fun Client.venueSpot(venueId: String, unitId: String): VenueSpot

/** The walk inside a venue to one unit, floor by floor, with steps in Mongolian and the floor on each. [from] is a
 * point on floor [fromLevel] (0 when null): the entrance a road route used, or a tapped spot. Needs door:read.
 * 404 not_found; 422 no_route where no drawn corridor joins the start and the unit. */
suspend fun Client.venueRoute(venueId: String, from: LonLat, unitId: String, fromLevel: Int? = null): VenueRoute

/** The whole way to a unit in one answer: the road to the right street entrance, then the walk inside. [mode] is
 * driving when null. 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. */
suspend fun Client.venueDirections(venueId: String, from: LonLat, unitId: String, mode: RouteMode? = null): VenueDirections

Water.kt ​

kotlin
/** A water point's fields, in WaterFeature.properties. */
data class WaterProperties(
    /** The first of drinking_water, water_point, water_tap, water_well, spring the OSM element carries. */
    val kind: String,
    val name: String? = null,
    /**
     * The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. null when
     * name is the OSM text. Show name. Packs before offline v12 lack it.
     */
    val nameOsm: String? = null,
    val nameEn: 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.
     */
    val drinkable: Boolean? = null,
    /** The OSM drinking_water tag as written. */
    val drinkingWater: String? = null,
    /** The OSM pump tag as written. */
    val pump: String? = null,
    /** The OSM element, as node/1 or way/1. */
    val osm: String,
)

/** One water point in an offline pack's water.geojson: an OSM node or way, a way at a point on its surface. */
data class WaterFeature(val type: String, val geometry: CrossingGeometry, val properties: WaterProperties)

/** Water points per drinkable value. */
data class WaterDrinkable(
    val yes: Int,
    val no: Int,
    /** OSM says nothing. */
    val unknown: Int,
)

/**
 * 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.
 */
data class PackWater(
    val type: String,
    /** Water points in the file, all Mongolia. */
    val written: Int,
    /** Per kind, all Mongolia. */
    val counts: Map<String, Int>,
    val named: Int,
    /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. null in packs before offline
     * v12. */
    val namesFolded: Int? = null,
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
    /** All Mongolia. */
    val drinkable: WaterDrinkable,
    /** A water point is near a route within this many metres of its line, as fuel and ger camps. */
    val nearM: Double,
    val rule: String,
    val ruleMn: String,
    val nearRule: String,
    val nearRuleMn: String,
    val source: String,
    val note: String,
    val noteMn: String,
    val features: List<WaterFeature>,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackWater
    }
}

/** A water point near the route. */
data class WaterOnRoute(
    val kind: String,
    val point: LonLat,
    val name: String?,
    val nameEn: String?,
    val drinkable: Boolean?,
    val drinkingWater: String?,
    val pump: String?,
    val osm: String,
    /** Along the route to its point nearest the water, m, rounded. A tie goes to the earlier pass. */
    val alongM: Double,
    /** From the route line, m, rounded. */
    val offM: Double,
    /** The OSM text when name was folded to one script, else null. Show name. */
    val nameOsm: String? = null,
)

/** What waterOnRoute returns. */
data class RouteWater(
    val nearM: Double,
    /** In route order. */
    val water: List<WaterOnRoute>,
    /** Of these points. unknown: OSM says nothing, not "not drinkable". */
    val drinkable: WaterDrinkable,
    /** Show with the points: from OSM tags, not field checked; a well may be dry, broken or locked. */
    val note: String,
    val noteMn: String,
    val rule: String,
    val ruleMn: String,
    val nearRule: String,
    val nearRuleMn: String,
    /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. null when the pack has no rule. */
    val nameRule: String? = null,
    val nameRuleMn: 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 nearM (2000), lane D's route pages.
 * Throws IllegalArgumentException for a line under 2 points or a nearM not above 0.
 */
fun waterOnRoute(water: PackWater, line: List<LonLat>, nearM: Double = water.nearM): RouteWater

Wetlands.kt ​

kotlin
/** A GeoJSON LineString: the stretch of the way inside the wetland area. */
data class WetlandGeometry(val type: String, val coordinates: List<LonLat>)

/** A stretch's fields, in WetlandFeature.properties. */
data class WetlandProperties(
    /** The area's OSM wetland tag, as marsh, bog, wet_meadow or fen; unknown when the area has none. */
    val wetland: String,
    /** The area's OSM element, as way/123 or relation/123. */
    val area: String,
    /** The area's OSM name. */
    val name: String? = null,
    /**
     * The OSM name as written, when it mixed Latin and Cyrillic in a word and name is folded to one script. null when
     * name is the OSM text. Show name. Packs before offline v12 lack it.
     */
    val nameOsm: String? = null,
    /** The stretch's length, km. */
    val km: Double,
    /** The car way's OSM id. null on a new track alone. */
    val wayId: Long? = null,
    val newTrack: Boolean,
    /** The way's OSM highway and surface tags, as on crossings. null on a new track alone. */
    val road: CrossingRoad? = null,
)

/** One stretch in an offline pack's wetlands.geojson: the part of one car way inside an OSM natural=wetland area. */
data class WetlandFeature(val type: String, val geometry: WetlandGeometry, val properties: WetlandProperties)

/** Stretches of one wetland kind, all Mongolia, and their length in km. */
data class WetlandCount(val stretches: Int, val km: Double)

/**
 * 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.
 */
data class PackWetlands(
    val type: String,
    /** Stretches in the file, all Mongolia. */
    val written: Int,
    val km: Double,
    /** OSM wetland areas the stretches lie in. */
    val areas: Int,
    /** Names in the file folded to one script, all Mongolia, and the rule, en and mn. null in packs before offline
     * v12. */
    val namesFolded: Int? = null,
    val nameRule: String? = null,
    val nameRuleMn: String? = null,
    /** Per wetland kind, all Mongolia. */
    val counts: Map<String, WetlandCount>,
    /** A route runs on a stretch where its line comes this close, m. */
    val matchM: Double,
    val rule: String,
    val ruleMn: String,
    val source: String,
    val note: String,
    val noteMn: String,
    val features: List<WetlandFeature>,
) {
    companion object {
        /** The file's text, as in the pack. */
        fun fromJson(text: String): PackWetlands
    }
}

/** A stretch through OSM wetland the route runs on. */
data class WetlandOnRoute(
    val wetland: String,
    val area: String,
    val name: String?,
    /** The whole stretch, km, from the pack. The route may run on part of it: onM. */
    val km: Double,
    val wayId: Long?,
    val newTrack: Boolean,
    val road: CrossingRoad?,
    /** Along the route to where it meets the stretch, m, rounded. */
    val alongM: Double,
    /** Metres of the stretch the route runs on, rounded. */
    val onM: Double,
    /** The stretch's first point the route meets, in route order. */
    val point: LonLat,
    /** The OSM text when name was folded to one script, else null. Show name. */
    val nameOsm: String? = null,
)

/** What wetlandsOnRoute returns. */
data class RouteWetlands(
    val matchM: Double,
    val minOnM: Double,
    /** In route order. */
    val stretches: List<WetlandOnRoute>,
    /** Sum of onM, m. */
    val onM: Double,
    /** Show with the stretches: from OSM tags, not field checked. */
    val note: String,
    val noteMn: String,
    val rule: String,
    val ruleMn: String,
    /** How the pack folds a name that mixes Latin and Cyrillic, en and mn. null when the pack has no rule. */
    val nameRule: String? = null,
    val nameRuleMn: 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. */
const val WETLAND_MIN_ON_M: Double

/**
 * 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 matchM; a piece is on the route when both its ends lie within
 * matchM 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 IllegalArgumentException for a line under 2 points.
 */
fun wetlandsOnRoute(wetlands: PackWetlands, line: List<LonLat>): RouteWetlands