UzMap docs

@uzmaps/api client

@uzmaps/api is a typed HTTP client for the UzMap server: search, geocoding, places, routing, distance matrix, isochrones, photos, usage and status. It has no runtime dependencies and needs only a global fetch, so it runs in browsers, Node, workers and other services.

The package is ESM only. Its package.json sets "type": "module" and the exports map has an import condition and no require condition, so require('@uzmaps/api') does not resolve.

bash
npm install @uzmaps/api

If you use TypeScript, also install @types/geojson. geometry(), districts() and IsochroneResponse.features are typed with Feature and FeatureCollection from the geojson type package, and @uzmaps/api does not declare that package as a dependency — its package.json has no dependencies or peerDependencies field, and the build's post-processing step does not add one. Without it, those three types resolve to any (or fail, depending on your skipLibCheck setting).

Creating a client#

ts
import { UzMapClient } from '@uzmaps/api';

const api = new UzMapClient({
  baseUrl: 'https://uzmaps.ndc.uz',
  apiKey: process.env.UZMAPS_KEY,
  language: 'uz',
});

createClient(opts?) is an exported function that does new UzMapClient(opts) and nothing else.

ClientOptions#

OptionTypeDefaultBehaviour
baseUrlstring''Server origin, e.g. https://uzmaps.ndc.uz. One trailing slash is stripped. The empty string means same origin: every request goes to a path such as /api/search.
languageLanguage'uz'Sent as lang on every language-aware call unless the call overrides it. Also the default language for route() bodies and for the two formatting helpers.
apiKeystringnoneSent as an X-API-Key header on every request the client makes. See API keys.
fetchtypeof fetchglobal fetchYour own fetch, for SSR, retries or instrumentation. When apiKey is also set, the client wraps your fetch and adds the header; supplying one does not silently drop the other.
timeoutnumber (ms)15000Applied to GET requests. POST requests (route, matrix, isochrone) use Math.max(timeout, 30000), because routing calls expand the road graph and a cold engine takes longer than a search.

Every option is optional; new UzMapClient() with no arguments is valid.

Instance properties#

PropertyTypeNotes
baseUrlstring (read-only)The normalised base URL.
languageLanguageWritable. Assigning a new value changes the default for every later call.
apiKeystring | undefined (read-only)Used by photoUrl().

API keys#

The key travels as an X-API-Key header rather than a query parameter so it stays out of URLs, and therefore out of the client's response cache keys, browser history and access logs that record query strings. The one exception is photoUrl(), which appends ?key= because an <img src> cannot carry a header; the server accepts either form.

A browser key is public by nature — it ships inside the page. What protects it is the origin allowlist attached to the key on the server, not secrecy. Restrict browser keys to your own domains; keep unrestricted keys on servers. Whether a key is required at all depends on the server: keys are enforced only when it runs with --require-key, and status() is served without a key in either case.

Conventions#

ConventionRule
DistancesMetres, everywhere: Route, RouteLeg, RouteStep, MatrixCell, IsochroneRequest.metres, nearby and lookup radii.
DurationsSeconds in responses (Route, RouteLeg, RouteStep, MatrixCell). IsochroneRequest.minutes is the one input in minutes.
Coordinates in method arguments(lat, lon), in that order: reverse(lat, lon), nearby(lat, lon), lookup(lat, lon, name). SearchOptions.near is { lat, lon }.
Coordinates in request bodies and geometry[lon, lat], GeoJSON order: RouteRequest.points, MatrixRequest.points/sources/targets, IsochroneRequest.point, Route.geometry.
Bounding boxes[minLon, minLat, maxLon, maxLat] for SearchOptions.bbox, Status.bounds and Route.bbox.
TimestampsISO 8601 UTC strings (UsageReport.from/to, UsagePoint.hour, UsageQuota.resets).
Opening hoursHoursSchedule.days has seven entries; each range's open and close are minutes from midnight, and close can exceed 1440 for overnight hours. OpenStatus is evaluated by the server in Asia/Tashkent time, not the caller's.
Languages'uz', 'uz-Cyrl', 'ru', 'en'.

Method index#

UzMapClient has 17 public methods. Every one except photoUrl returns a Promise.

MethodRequestCachedReturns
status()GET /api/statusnoStatus
categories()GET /api/categoriesyes{ groups: CategoryGroupInfo[]; categories: CategoryInfo[] }
search(query, options?)GET /api/searchnoSearchResponse
autocomplete(query, options?)GET /api/autocompletenoSearchResponse
reverse(lat, lon, options?)GET /api/reversenoReverseResponse
nearby(lat, lon, options?)GET /api/nearbyno{ results: SearchResult[] }
lookup(lat, lon, name, options?)GET /api/lookupno{ result: SearchResult | null }
place(id, options?)GET /api/place/{id}yesPlaceDetails
placeByOsm(type, id, options?)GET /api/place/osm/{type}/{id}yesPlaceDetails
geometry(id, signal?)GET /api/geometry/{id}yesFeature
districts(signal?)GET /api/districtsyesFeatureCollection
photo(wikidata, signal?)GET /api/photoyes{ photo: Photo | null }
photoUrl(photo)nonestring
route(request, signal?)POST /api/routenoRouteResponse
matrix(request, signal?)POST /api/matrixnoMatrixResponse
isochrone(request, signal?)POST /api/isochronenoIsochroneResponse
usage(options?)GET /api/usagenoUsageReport

All fifteen paths are registered in services/server/internal/server/server.go (routes()).

Methods#

status#

ts
status(): Promise<Status>

Capability probe. The server serves /api/status without a key even when it enforces keys elsewhere, so this call works before you have one. routing is a live health check against the routing engine with a 1.5 s budget, not a configuration flag.

ts
const s = await api.status();
if (!s.routing) console.warn('routing engine is not answering');
console.log(`${s.docs} places indexed, data built ${s.built_at}`);
if (s.sources?.terrain) console.log('terrain tiles available');

categories#

ts
categories(): Promise<{ groups: CategoryGroupInfo[]; categories: CategoryInfo[] }>

The taxonomy the search index uses. Cached for the life of the client. Category ids are dotted, e.g. food.cafe, health.pharmacy. Read the group from CategoryInfo.group rather than from the prefix: it usually matches, but not always — transport.parking belongs to the auto group and culture.theme_park to leisure.

ts
const { groups, categories } = await api.categories();
const food = categories.filter((c) => c.group === 'food');
console.log(groups.length, 'groups;', food.map((c) => c.id));
ts
search(query: string, options?: SearchOptions): Promise<SearchResponse>

Full search. SearchOptions fields and how they reach the server:

OptionTypeQuery parameterNotes
near{ lat: number; lon: number }lat, lonThe server ignores the pair unless both are non-zero.
biasnumberbiasServer default 0.5.
limitnumberlimitServer default 10, capped at 50.
kindsPlaceKind[]kinds (comma-joined)Restrict to place, street, poi, address, nature.
categorystringcategoryA category id such as food.cafe, or a whole group as group:food.
bbox[minLon, minLat, maxLon, maxLat]bbox (comma-joined)
languageLanguagelangDefaults to the client's language.
signalAbortSignalAborts the request.

Options that are undefined are not sent. SearchResponse.intent is present when the query itself parses as a category ("dorixona" → pharmacy); intent.browse says the server treated it as a browse rather than a name match. An empty query with a category browses that category — the demo app does this to list everything in a group near the map centre.

ts
const { results, intent, took_ms } = await api.search('dorixona', {
  near: { lat: 41.2995, lon: 69.2401 },
  limit: 5,
});
if (intent) console.log(`category browse: ${intent.label} (${intent.category})`);
for (const r of results) console.log(r.name, '—', r.label, r.lat, r.lon);
console.log(`${took_ms} ms`);

// Everything in the "food" group inside a viewport, nearest first
const inView = await api.search('', {
  category: 'group:food',
  near: { lat: 41.2995, lon: 69.2401 },
  bbox: [69.2, 41.27, 69.3, 41.33],
  limit: 40,
});

autocomplete#

ts
autocomplete(query: string, options?: SearchOptions): Promise<SearchResponse>

As-you-type variant with the same options. The client sets limit: 8 unless you pass your own; the server caps autocomplete at 12 regardless. Pass a signal and abort the previous request on every keystroke, otherwise slow responses arrive out of order.

ts
let pending: AbortController | undefined;

async function suggest(text: string) {
  pending?.abort();
  pending = new AbortController();
  const { results } = await api.autocomplete(text, { signal: pending.signal });
  return results.map((r) => r.name);
}

reverse#

ts
reverse(
  lat: number,
  lon: number,
  options?: { language?: Language; signal?: AbortSignal },
): Promise<ReverseResponse>

Coordinates to address. Both numbers are sent with six decimal places (about 0.1 m), which keeps URLs short and stable. The server answers 400 if either is exactly 0.

label is the full address line, short the street and house number (or the best available area name). street is the nearest named street; its distance is the figure the server ranked candidate streets by — the metre distance to the street multiplied by a road-class weight (0.8 for motorway/trunk/primary/secondary, 1.0 for tertiary and residential-class streets, 1.8 for service roads, 2.5 for tracks and paths), rounded to one decimal — so it is comparable between streets but is not a measured distance. place is the nearest indexed place; admins lists the enclosing administrative areas.

ts
const r = await api.reverse(41.2995, 69.2401, { language: 'ru' });
console.log(r.short);              // "улица Амира Темура, 12"
console.log(r.label);              // full line
if (r.street) console.log(r.street.name);

nearby#

ts
nearby(
  lat: number,
  lon: number,
  options?: {
    radius?: number;        // metres; server default 500, capped at 20 000
    category?: string;      // e.g. 'food.cafe'
    kinds?: PlaceKind[];    // server default ['poi']
    limit?: number;         // server default 20
    language?: Language;
    signal?: AbortSignal;
  },
): Promise<{ results: SearchResult[] }>

Places around a point. Each result carries distance in metres from the point.

ts
const { results } = await api.nearby(41.2995, 69.2401, {
  radius: 800,
  category: 'food.cafe',
  limit: 10,
});
for (const r of results) console.log(r.name, Math.round(r.distance ?? 0), 'm');

lookup#

ts
lookup(
  lat: number,
  lon: number,
  name: string,
  options?: { radius?: number; language?: Language; signal?: AbortSignal },
): Promise<{ result: SearchResult | null }>

Resolves a label the user clicked on the basemap — a name plus a position — to an indexed place, so a tile feature can be turned into something place() can open. radius is in metres; the server default is 120. result is null when nothing matches within the radius; that is an answer, not an error. Coordinates are sent with six decimal places.

ts
// From a map click: feature name and its lngLat
const { result } = await api.lookup(41.3111, 69.2797, 'Chorsu bozori');
if (result) {
  const details = await api.place(result.id);
  console.log(details.full_address);
}

place#

ts
place(id: number, options?: { language?: Language; signal?: AbortSignal }): Promise<PlaceDetails>

Full details for an indexed place by its SearchResult.id. Cached per id and language. The server answers 400 for a non-numeric id and 404 when the id is unknown.

PlaceDetails extends SearchResult with:

FieldMeaning
osm{ type: 'n' | 'w' | 'r', id, url } — the OpenStreetMap object and a link to it.
tagsEvery OSM tag on the object.
full_addressFormatted address line in the requested language.
detailsSelected tags lifted out of tags: phone, website, instagram, telegram, email, cuisine, wheelchair, opening_hours, and others under the index signature (including wikidata when tagged). hours and open_status are present whenever opening_hours is non-empty; if the parser could not read the value, hours.unparsed is true and open_status.known is false.
photo_hint/api/photo?wikidata=Q… when the server has photos enabled and the object has a wikidata tag. Use photo() with the Q-id rather than fetching this path yourself.
nearbyUp to six named POIs within 350 m, excluding the place itself.
geometry_url/api/geometry/{id} when the entry has stored geometry and is a street, place, nature or POI entry. Check it before calling geometry().
population, admin_levelAlways sent; 0 when the place has neither.
ts
const details = await api.place(12345, { language: 'en' });
console.log(details.full_address);
if (details.details.open_status?.open) console.log('open now');
if (details.geometry_url) {
  const feature = await api.geometry(details.id);
  console.log(feature.geometry.type); // 'Polygon' | 'MultiPolygon' | 'MultiLineString'
}

placeByOsm#

ts
placeByOsm(
  type: 'n' | 'w' | 'r',
  id: number,
  options?: { language?: Language; signal?: AbortSignal },
): Promise<PlaceDetails>

The same details addressed by OpenStreetMap object type (node, way, relation) and OSM id instead of the index id. Cached. 404 when the object is not in the index.

ts
const tashkent = await api.placeByOsm('r', 2214960);
console.log(tashkent.name, tashkent.population);

geometry#

ts
geometry(id: number, signal?: AbortSignal): Promise<Feature>

A GeoJSON Feature for a place, in [lon, lat] coordinates, with properties.id, properties.name and properties.kind. Cached. The geometry type depends on what the place is:

PlaceGeometry
Street, riverMultiLineString
Administrative area (an OSM relation of kind place)MultiPolygon (properties.kind is 'admin')
Anything else with an outlinePolygon

The server answers 400 for a non-numeric id and 404 when the id is unknown or the place has no geometry. Check PlaceDetails.geometry_url first.

ts
const outline = await api.geometry(12345);
map.highlight.setGeometry(outline); // e.g. with @uzmaps/engine

districts#

ts
districts(signal?: AbortSignal): Promise<FeatureCollection>

Label points for regions, cities, districts and mahallas as a GeoJSON FeatureCollection of Point features. Cached, because the set only changes when the map data is rebuilt. Each feature's properties carry:

PropertyMeaning
namePrimary name.
name:<lang>Localised names, one property per available language.
levelOSM admin level: 6 region (viloyat), 7 city, 8 city district, 9 rural district, 10 mahalla.
extentDiagonal extent of the area in metres; use it to hold back small mahalla labels at low zoom.
ts
const { features } = await api.districts();
const mahallas = features.filter((f) => f.properties?.level === 10);
console.log(mahallas.length, 'mahallas');

photo#

ts
photo(wikidata: string, signal?: AbortSignal): Promise<{ photo: Photo | null }>

A Wikimedia Commons photo for a Wikidata Q-id, resolved and proxied by the server. Cached. photo is null when the item has no usable image. The server answers 404 when it runs without photo support (Status.photos is false) and 502 when Wikimedia fails.

Photo.url is server-relative (/api/photo/img/Q….jpg) — pass the object through photoUrl() before using it. attribution is always present and is the text you must display; it is author, license and the words Wikimedia Commons, whichever are available, joined with ·. license_url is the link for the licence and is not part of that string.

photoUrl#

ts
photoUrl(photo: Photo): string

Turns Photo.url into something an <img src> can load. No request is made. The rules, in order:

CaseResult
photo.url starts with httpReturned unchanged. It points somewhere else and must not receive your key.
Otherwise, client has no apiKeybaseUrl + photo.url
OtherwisebaseUrl + photo.url with key=<apiKey> appended, using & if the URL already has a ? and ? if not. The key is URL-encoded.

The key goes in the query string here, unlike every other call, because the browser issues the image request itself and there is no place to attach a header. /api/photo/img/ is gated like the rest of /api/, so without the key the image would be refused on a server that enforces keys.

ts
const img = document.querySelector<HTMLImageElement>('#photo')!;
const caption = document.querySelector<HTMLElement>('#caption')!;

const details = await api.place(12345);
const qid = details.details.wikidata;
if (typeof qid === 'string') {
  const { photo } = await api.photo(qid);
  if (photo) {
    img.src = api.photoUrl(photo);
    caption.textContent = photo.attribution;
  }
}

route#

ts
route(request: RouteRequest, signal?: AbortSignal): Promise<RouteResponse>

Turn-by-turn routing through two or more points. The client sends { language: api.language, alternatives: 2, ...request }, so those two defaults apply unless you override them.

FieldTypeNotes
points[number, number][][lon, lat]. At least two, or the server answers 400. First and last are stops; any in between are pass-through points.
modeRouteModecar, foot, bike, taxi, truck, motorcycle. Omitted or unrecognised is routed as car, and the response reports mode: 'car'.
languageLanguageLanguage of instruction text. Defaults to the client's language.
alternativesnumber0–3. Anything outside that range is reset to 2 by the server.
avoid_tollsboolean
avoid_highwaysboolean
shortestbooleanShortest distance rather than fastest time.
headingnumberDegrees. Applied to the first point only — the direction the vehicle is already travelling.

The server routing engine also accepts 'bus' as a mode, but the client's RouteMode union does not include it, so TypeScript will reject it without a cast.

RouteResponse.routes[0] is the main route; extra entries are the alternatives. Each Route has distance in metres, duration in seconds, geometry as [lon, lat][], bbox, legs with steps, a summary string, recommended, and has_toll / has_highway / has_ferry when true. Each RouteStep's geometry_start and geometry_end index into Route.geometry.

The server answers 502 when the engine fails, which includes "no route found".

ts
const { routes, warnings } = await api.route({
  points: [[69.2401, 41.2995], [69.2870, 41.3131]], // [lon, lat]
  mode: 'car',
  avoid_tolls: true,
});
const best = routes.find((r) => r.recommended) ?? routes[0];
console.log(best.summary, best.distance, 'm', best.duration, 's');
for (const step of best.legs[0].steps) console.log(step.instruction);
if (warnings?.length) console.warn(warnings);

matrix#

ts
matrix(request: MatrixRequest, signal?: AbortSignal): Promise<MatrixResponse>

Travel cost between every source and every target — the primitive behind "which courier is closest". Sent as POST because a 50×50 matrix carries 100 coordinates, which overruns URL length limits on real proxies.

FieldTypeNotes
points[number, number][]Symmetric shorthand: every point to every point. When present and non-empty, the server ignores sources and targets.
sources[number, number][][lon, lat]. Use with targets when the two sets differ.
targets[number, number][]
modeRouteModeAs for route().
avoid_tolls, avoid_highways, shortestbooleanAs for route().

Limits enforced by the server, each answered with 400: sources and targets must both be non-empty, and sources × targets must not exceed 2 500 pairs.

MatrixResponse.cells is the flat list of every pair; rows is the same cells grouped so rows[source][target] works. A null distance or duration means no path exists between that pair — a real answer, not an error.

The server gives a matrix 60 s; the client gives it Math.max(timeout, 30000). For large matrices set timeout above 30 000 when you create the client, or the client will abort first.

ts
const m = await api.matrix({
  sources: [[69.2401, 41.2995]],
  targets: [[69.2870, 41.3131], [69.2200, 41.3300], [69.3100, 41.2800]],
  mode: 'car',
});
const nearest = m.rows[0]
  .filter((c) => c.duration !== null)
  .sort((a, b) => a.duration! - b.duration!)[0];
console.log('closest target:', nearest.target, nearest.duration, 's');

isochrone#

ts
isochrone(request: IsochroneRequest, signal?: AbortSignal): Promise<IsochroneResponse>

The area reachable from a point within given travel times or distances, as a GeoJSON FeatureCollection with mode and engine added at the top level.

FieldTypeNotes
point[number, number][lon, lat]. Required.
modeRouteModeAs for route().
minutesnumber[]Travel-time contours in minutes.
metresnumber[]Travel-distance contours in metres.
polygonsbooleantrue for filled areas, false for lines. Pass it explicitly.
denoisenumber0–1. Drops small disconnected islands.
generalizenumberMetres of simplification; trades fidelity for payload size.

Rules enforced by the server's handler, each answered with 400: point must be given and not [0, 0], and exactly one of minutes or metres must be given, with at most 6 contours in total. A contour value of zero or less is rejected as well, but by the routing adapter rather than the handler, so it comes back as 502 with the message contour minutes must be positive or contour metres must be positive.

On polygons: the type's own comment describes filled areas as the default, but that is not what happens through this client. The client POSTs the request as-is, and the server decodes the POST body into a plain boolean, so an absent polygons is false and you get lines. Only the server's GET form defaults to polygons. If you want areas to fill or hit-test, send polygons: true.

Each feature's properties carry contour_minutes or contour_metres (in metres, converted from the engine's kilometres), whichever axis you asked for. The server answers 502 when no reachable area is found.

ts
const iso = await api.isochrone({
  point: [69.2401, 41.2995],
  mode: 'foot',
  minutes: [5, 10, 15],
  polygons: true,
});
for (const f of iso.features) {
  console.log(f.properties?.contour_minutes, 'min:', f.geometry.type);
}

usage#

ts
usage(options?: { days?: number; series?: boolean; signal?: AbortSignal }): Promise<UsageReport>

Traffic recorded for this client's key. A key can only read its own usage, so no other credential is needed, and reading it is not billable and does not count against the monthly quota it reports.

OptionQuery parameterNotes
daysdaysRolling window of 1–366 days ending now. Outside that range the server answers 400. Omit for the current UTC calendar month, which is the period a bill covers.
seriesseries=1Adds the hourly series breakdown. Opt-in because it can be hundreds of points.

Unlike the rest of the API, this endpoint checks the key itself even on a server that is not enforcing keys: a request with no key, or with an unknown one, gets 401. The server answers 503 when usage metering or API keys are not configured on it.

UsageReport.products is per product (tiles, search, geocoding, places, routing, matrix, isochrone, static, other), each with requests (served and billable) and denied (refused — wrong origin, disabled key, over the rate limit — and never billable). quota is present only for keys with a monthly allowance, so through this endpoint remaining is always 0 or more; the -1 that the UsageQuota type documents for unlimited keys is never emitted here, because an unlimited key gets no quota object at all.

ts
const report = await api.usage({ days: 7, series: true });
console.log(report.name, report.requests, 'requests,', report.denied, 'denied');
for (const p of report.products) console.log(p.product, p.requests);
if (report.quota) console.log(`${report.quota.remaining} left until ${report.quota.resets}`);

Errors#

UzMapApiError#

ts
class UzMapApiError extends Error {
  constructor(public status: number, message: string);
  name: 'UzMapApiError';
}

Thrown for every HTTP response whose status is not 2xx. status is the HTTP status. message is the error field of the server's JSON body when there is one, otherwise the HTTP status text.

Only HTTP responses become UzMapApiError. A network failure, a timeout or an aborted signal rejects with whatever error fetch itself raises; the client does not convert those.

ts
import { UzMapApiError } from '@uzmaps/api';

try {
  await api.search('Chilonzor');
} catch (e) {
  if (e instanceof UzMapApiError) {
    if (e.status === 403) console.error('key disabled or origin not allowed');
    else console.error(e.status, e.message);
  } else {
    throw e; // network, timeout or abort
  }
}

Status codes#

StatusWhen
400Bad or missing parameters: reverse/lookup with a zero coordinate, non-numeric place/geometry id, route with fewer than two points, matrix with no points or more than 2 500 pairs, isochrone with no point, no contours, both minutes and metres, or more than 6 contours, usage with days outside 1–366, or an unparsable JSON body.
401No key sent, or the key is unknown. Also usage() with a missing or unknown key, even on a server that does not otherwise enforce keys.
403The key is disabled; the key is restricted to origins and the request sent no Origin or Referer; or the request's origin is not on the key's allowlist.
404place/placeByOsm id not in the index; geometry for a place without geometry; photo() on a server without photo support.
429The key's per-minute rate limit was exceeded, or its monthly quota is exhausted. The quota response carries a Retry-After header with the seconds until the allowance resets.
500Database error while reading a place, or while reading usage.
502The routing engine or the photo upstream failed, including "no route found", "no reachable area found" and an isochrone contour value of zero or less.
503The search index is not built (search, autocomplete, reverse, nearby, lookup, place, geometry, districts); the routing engine is not configured (matrix, isochrone); usage metering or keys are not configured (usage).

One thing to know about the key-related failures (401, 403 and both kinds of 429): the server's key gate writes the HTTP status text into the error field and its explanation into a separate message field. The client reads only error, so e.message will be 'Unauthorized', 'Forbidden' or 'Too Many Requests', and the sentence saying why is not surfaced. Branch on e.status. The 401 from usage() is the exception: its error field carries the sentence.

Caching#

Six methods cache their result in the client instance, keyed by the full request URL (which includes lang), for as long as the client lives. A request that fails is evicted so the next call retries. There is no other eviction; create a new client to start clean.

CachedNot cached
categories, place, placeByOsm, geometry, districts, photostatus, search, autocomplete, reverse, nearby, lookup, usage, route, matrix, isochrone

The cached set is data that only changes when the map is rebuilt; the uncached set is either query-dependent or, for the POST calls, large bodies that rarely repeat exactly. Because the cache stores the promise rather than the value, two concurrent calls for the same place share one request.

Timeouts and cancellation#

Call typeClient timeoutServer budget
GETtimeout (default 15 000 ms)
routeMath.max(timeout, 30000)25 s
matrix, isochroneMath.max(timeout, 30000)60 s

Every method that accepts a signal (or options.signal) aborts the underlying request when the signal fires. A timeout or abort rejects with the error fetch raises, not with UzMapApiError.

Formatting helpers#

ts
formatDistance(metres: number, lang?: Language): string   // lang defaults to 'uz'
formatDuration(seconds: number, lang?: Language): string  // lang defaults to 'uz'

formatDistance:

InputOutput
under 950 mRounded to the nearest 10 m, e.g. 740 m
950 m to under 10 kmOne decimal in km with a trailing .0 dropped, e.g. 1.2 km, 8 km
10 km and overWhole km, e.g. 23 km

formatDuration rounds to whole minutes, shows at least 1 min, and switches to hours from 60 minutes: 45 daq, 1 s, 1 s 20 daq (Uzbek); 2 h 5 min (English).

Units by language:

langDistanceDuration
ruм, кмч, мин
enm, kmh, min
uz, uz-Cyrlm, kms, daq
ts
import { formatDistance, formatDuration } from '@uzmaps/api';

formatDistance(1234);        // '1.2 km'
formatDistance(1234, 'ru');  // '1.2 км'
formatDuration(4500, 'en');  // '1 h 15 min'

Types#

All of the following are exported from @uzmaps/api and reproduced from packages/api/src/index.ts.

ts
type Language = 'uz' | 'uz-Cyrl' | 'ru' | 'en';
type PlaceKind = 'place' | 'street' | 'poi' | 'address' | 'nature';

interface Address {
  street?: string;
  housenumber?: string;
  neighbourhood?: string;
  suburb?: string;
  city?: string;
  district?: string;
  region?: string;
  postcode?: string;
}

interface SearchResult {
  id: number;
  kind: PlaceKind;
  category: string;
  group: string;
  icon: string;
  name: string;
  label: string;           // secondary line, e.g. "Kafe · Amir Temur ko'chasi, 12, Toshkent"
  category_label: string;
  lat: number;
  lon: number;
  bbox: [number, number, number, number];
  distance?: number;       // metres from `near` / the nearby point, when one was given
  address: Address;
  names?: Record<string, string>;
  matched?: string[];
  rank: number;
  reason?: string;
}

interface SearchIntent { category: string; browse: boolean; label: string; icon: string }

interface SearchResponse {
  query: string;
  results: SearchResult[];
  took_ms: number;
  intent?: SearchIntent;
}

interface SearchOptions {
  near?: { lat: number; lon: number };
  bias?: number;
  limit?: number;
  kinds?: PlaceKind[];
  category?: string;
  bbox?: [number, number, number, number];
  language?: Language;
  signal?: AbortSignal;
}

Places#

ts
interface OpenStatus {
  open: boolean;
  known: boolean;
  all_day?: boolean;
  next_change?: string;   // "HH:MM"
  closes_soon?: boolean;
  opens_soon?: boolean;
  next_day?: number;      // 0 = today, 1 = tomorrow, ...
}

interface HoursSchedule {
  raw: string;
  all_day: boolean;
  days: { open: number; close: number }[][];  // 7 days; minutes from midnight
  unparsed?: boolean;
}

interface PlaceDetails extends SearchResult {
  osm: { type: 'n' | 'w' | 'r'; id: number; url: string };
  tags: Record<string, string>;
  population?: number;
  admin_level?: number;
  full_address: string;
  details: {
    phone?: string;
    website?: string;
    instagram?: string;
    telegram?: string;
    email?: string;
    cuisine?: string;
    wheelchair?: string;
    opening_hours?: string;
    hours?: HoursSchedule;
    open_status?: OpenStatus;
    [k: string]: unknown;
  };
  photo_hint?: string;
  nearby: SearchResult[];
  geometry_url?: string;
}

interface Photo {
  url: string;          // server-relative; resolve with photoUrl()
  source_url: string;   // the Commons file page
  author?: string;
  license?: string;
  license_url?: string;
  title?: string;
  width?: number;
  height?: number;
  attribution: string;
}

interface ReverseResponse {
  lat: number;
  lon: number;
  address: Address;
  label: string;
  short: string;
  street?: { id: number; name: string; distance: number; lon: number; lat: number };
  place?: SearchResult;
  admins?: { level: number; name: string; osm_id: number }[];
}

interface CategoryGroupInfo { id: string; icon: string; color: string; label: Record<string, string> }
interface CategoryInfo {
  id: string;
  group: string;
  icon: string;
  label: Record<string, string>;
  synonyms?: string[];
  rank: number;
}

Routing#

ts
type RouteMode = 'car' | 'foot' | 'bike' | 'taxi' | 'truck' | 'motorcycle';

interface RouteRequest {
  points: [number, number][];   // [lon, lat]
  mode?: RouteMode;
  language?: Language;
  alternatives?: number;
  avoid_tolls?: boolean;
  avoid_highways?: boolean;
  shortest?: boolean;
  heading?: number;
}

interface RouteStep {
  type: string;
  modifier: string;
  instruction: string;
  verbal?: string;
  street_names?: string[];
  distance: number;        // metres
  duration: number;        // seconds
  geometry_start: number;  // index into Route.geometry
  geometry_end: number;
  roundabout_exit?: number;
  toll?: boolean;
  highway?: boolean;
  ferry?: boolean;
  sign?: { exit_number?: string[]; branch?: string[]; toward?: string[] };
}

interface RouteLeg { distance: number; duration: number; steps: RouteStep[] }

interface Route {
  id: string;
  mode: RouteMode;
  distance: number;               // metres
  duration: number;               // seconds
  geometry: [number, number][];   // [lon, lat]
  bbox: [number, number, number, number];  // [minLon, minLat, maxLon, maxLat]
  legs: RouteLeg[];
  summary: string;
  has_toll?: boolean;
  has_highway?: boolean;
  has_ferry?: boolean;
  recommended: boolean;
}

interface RouteResponse { routes: Route[]; language: string; engine: string; warnings?: string[] }

Matrix and isochrone#

ts
interface MatrixRequest {
  points?: [number, number][];    // symmetric shorthand; [lon, lat]
  sources?: [number, number][];
  targets?: [number, number][];
  mode?: RouteMode;
  avoid_tolls?: boolean;
  avoid_highways?: boolean;
  shortest?: boolean;
}

interface MatrixCell {
  source: number;
  target: number;
  distance: number | null;   // metres; null = no path
  duration: number | null;   // seconds; null = no path
}

interface MatrixResponse {
  mode: RouteMode;
  sources: number;
  targets: number;
  cells: MatrixCell[];
  rows: MatrixCell[][];      // rows[source][target]
  engine: string;
}

interface IsochroneRequest {
  point: [number, number];   // [lon, lat]
  mode?: RouteMode;
  minutes?: number[];        // mutually exclusive with metres
  metres?: number[];         // mutually exclusive with minutes
  polygons?: boolean;        // send explicitly; absent means lines through this client
  denoise?: number;          // 0..1
  generalize?: number;       // metres
}

interface IsochroneResponse {
  type: 'FeatureCollection';
  features: FeatureCollection['features'];   // each with contour_minutes or contour_metres
  mode: RouteMode;
  engine: string;
}

Usage#

ts
type UsageProduct =
  | 'tiles' | 'search' | 'geocoding' | 'places'
  | 'routing' | 'matrix' | 'isochrone' | 'static' | 'other';

interface UsageTotal {
  product: UsageProduct;
  requests: number;   // served, billable
  denied: number;     // refused, never billable
}

interface UsagePoint extends UsageTotal {
  hour: string;       // start of the hour, ISO 8601 UTC
}

interface UsageQuota {
  limit: number;
  used: number;
  remaining: number;  // the type says -1 when unlimited; /api/usage omits `quota` for unlimited keys instead
  resets: string;     // ISO 8601 UTC
}

interface UsageReport {
  key: string;
  name: string;
  from: string;
  to: string;
  requests: number;
  denied: number;
  products: UsageTotal[];
  series?: UsagePoint[];   // only with { series: true }
  quota?: UsageQuota;      // only for keys with a monthly limit
}

Which paths count under which product is decided by the server: search covers search and autocomplete; geocoding covers reverse and lookup; places covers nearby, categories, districts, place, geometry and photos; routing, matrix and isochrone are one endpoint each; tiles covers everything under /tiles/, /terrain/, /fonts/, /sprites/ and /styles/. status() and usage() are not billable and never appear in the report.

Status#

ts
interface StatusSources {
  tiles: string[];       // every served PMTiles archive, e.g. ['uzbekistan', 'buildings-ml']
  terrain: boolean;
  places: boolean;       // Overture places overlay
  buildings_ml: boolean; // Microsoft ML building footprints overlay
}

interface Status {
  uptime_s: number;
  tiles: boolean;
  search: boolean;
  routing: boolean;      // live health check
  photos: boolean;
  docs: number;          // indexed places
  built_at: string;
  attribution: string;
  bounds?: [number, number, number, number];   // [minLon, minLat, maxLon, maxLat]
  maxzoom?: number;
  sources?: StatusSources;   // absent on older servers
}

The current server also sends sources.furniture (a boolean for the street-furniture overlay), which StatusSources does not declare. Read it with a cast if you need it.

Map data © OpenStreetMap contributors, licensed under the ODbL. © 2026 National Development Community.