JavaScript / TypeScript SDK
@mapmetrics/geocoder is a TypeScript client for the v2 geocoding API — autocomplete, place retrieval, forward/reverse geocoding, routing, and isochrones. Zero runtime dependencies, ships ESM + CJS + types, and runs anywhere fetch exists: Node 18+, browsers, and edge runtimes like Cloudflare Workers.
Not published yet
npm install @mapmetrics/geocoder will work once the package ships. Until then, consume it from the NPM/ directory of MapMetrics/geocoder-sdk directly (e.g. npm install github:MapMetrics/geocoder-sdk#path:NPM or a local file: dependency).
Install
npm install @mapmetrics/geocoderQuickstart
import { MapAtlas } from '@mapmetrics/geocoder';
const mapatlas = new MapAtlas({ token: 'YOUR_API_KEY' });
// One session covers a whole search — every keystroke, then the pick.
const session = mapatlas.geocoding.createSession();
// As the user types. Debounced for you; suggestions carry no coordinates.
const results = await session.suggest('Nieuwezijds');
// When they pick one, hand back the suggestion object — not an id.
const place = await session.retrieve(results[0]);
console.log(place.latitude, place.longitude);
// Resolving several at once is both fewer round trips and cheaper billing —
// see Sessions & Billing.
const places = await session.retrieveBatch(results.slice(0, 3));
// One-shot lookups, when there's no user typing to debounce. Both return GeoJSON.
await mapatlas.geocoding.search('Nieuwezijds Voorburgwal 147', { country: 'nl' });
await mapatlas.geocoding.reverse(52.37, 4.89);Constructor options
new MapAtlas({ token: 'YOUR_API_KEY' });
// or, if you rotate/refresh tokens yourself:
new MapAtlas({ getToken: async () => await fetchFreshToken() });| Option | Default | Notes |
|---|---|---|
token | — | Exactly one of token / getToken is required. |
getToken | — | Called fresh on every request — never cached by this package. |
baseUrl | https://gateway.mapmetrics-atlas.net | Override for testing or a self-hosted gateway. |
tier | 'v2' | 'osm' routes to the free, rate-limited OpenStreetMap-only tier. |
debounceMs | 150 | Debounces rapid suggest() calls. 0 disables. |
The reactive layer: useAutocomplete
@mapmetrics/geocoder/react is a separate subpath export with a headless useAutocomplete hook — it owns the Session (and therefore billing), debouncing, request cancellation, and error handling. Importing the core @mapmetrics/geocoder entry never pulls React into your bundle; react (>=18) is an optional peer dependency, only needed if you import this subpath.
npm install react react-dom # if your app doesn't already have themimport { useMemo, useState } from 'react';
import { MapAtlas } from '@mapmetrics/geocoder';
import { useAutocomplete } from '@mapmetrics/geocoder/react';
import type { Suggestion } from '@mapmetrics/geocoder';
function AddressField() {
const client = useMemo(() => new MapAtlas({ token: 'YOUR_API_KEY' }), []);
const [selecting, setSelecting] = useState(false);
const {
query, setQuery, // controlled input value
suggestions, // Suggestion[] — updates as the user types
isLoading, // a request is in flight
error, // typed MapAtlasError | null
select, // (s: Suggestion) => Promise<RetrievedPlace>
selected, // RetrievedPlace | null — last successful selection
reset, // clear query, suggestions, error and selection
} = useAutocomplete({ client, country: 'nl', minLength: 2 });
async function handleSelect(s: Suggestion) {
setSelecting(true);
try {
const place = await select(s);
console.log(place.latitude, place.longitude);
} catch {
// already surfaced via `error` below
} finally {
setSelecting(false);
}
}
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search an address…"
/>
{isLoading && <span>Searching…</span>}
{error && <span role="alert">{error.name}: {error.message}</span>}
<ul>
{suggestions.map((s) => (
<li key={s.ord}>
<button type="button" disabled={selecting} onClick={() => handleSelect(s)}>
{s.placeName ?? s.text}
</button>
</li>
))}
</ul>
{selected && (
<p>
{selected.latitude}, {selected.longitude}
<button type="button" onClick={reset}>Clear</button>
</p>
)}
</div>
);
}Options: client (required), country, proximity, minLength (default 2 — below it, no request fires and suggestions is cleared), debounceMs (extra debounce stacked on top of the client's own — leave unset unless you need a delay different from client.debounceMs).
Session lifecycle matches the core Session: one session per client, reused across every keystroke; select() retrieves and closes it, and the next setQuery() reopens one automatically. suggestCount and isOpen on the hook's return value mirror Session.suggestCount / Session.isOpen.
Safety guarantees: out-of-order responses are discarded (a slow response for an earlier keystroke never overwrites a later one); no setState fires after unmount; React 18 <StrictMode>-safe (no duplicate session, no duplicate request on double-invoked effects).
Routing & isochrones
mapatlas.routing and the top-level mapatlas.isochrone() wrap the gateway's Valhalla-backed endpoints — directions, matrix, map matching, and route optimization. costing is a strict union ('auto' | 'bicycle' | 'bus' | 'truck' | 'taxi' | 'motor_scooter' | 'pedestrian' | 'bikeshare'), not a bare string, and this client always hits /optimization/, not /optimize/ (the latter 404s). See the README for the full request/response shapes.
The OSM tier
tier: 'osm' talks to the free, OpenStreetMap-only endpoints (/osm-geocode/, /osm-reverse/, /osm-autocomplete/) via the osm-geocode scope. It has no session/retrieve flow — search(), reverse(), and autocomplete() are the only three operations, results already carry coordinates, and createSession() throws TierUnsupportedError. Rate-limited to 10,000 requests/key/day plus a global monthly cap; hitting it throws QuotaExceededError.
Self-hosting
The free tier's engine is open source: MapMetrics/atlas-osm-geocoder, deployable on Cloudflare Workers with a prebuilt planet bundle. Point this package at your own instance with MapAtlas.selfHosted():
const mapatlas = MapAtlas.selfHosted({ baseUrl: 'https://my-worker.workers.dev' });
await mapatlas.geocoding.search('Nieuwezijds Voorburgwal 147');
await mapatlas.geocoding.autocomplete('Nieuwezijds');
await mapatlas.geocoding.reverse(52.37, 4.89);A self-hosted instance takes no token and uses different paths (/search, /reverse, /autocomplete — no /osm- prefix, no token parameter); selfHosted() never sends a credential to it, even if you pass one by mistake. createSession() and session-based retrieval are unavailable on a self-hosted client, same as on tier: 'osm' — there's no session concept on this engine.
Errors
All errors extend MapAtlasError:
TokenNotFoundError— key was never provisioned.TokenInactiveError— key exists but is deactivated.ScopeError— key lacks the scope this operation requires.OriginRequiredError— key is origin-restricted, request sent noOriginheader. Native/server clients can never satisfy this — see Choosing a key for native apps.OriginNotAllowedError— request'sOriginisn't on the key's allow-list.QuotaExceededError— OSM tier cap exhausted; carriesselfHostUrl.TierUnsupportedError— thrown bycreateSession()on theosmtier and onselfHosted()clients, and byautocomplete()on thev2tier.NetworkError— the request never completed, or the response wasn't parseable JSON.
import { MapAtlasError, ScopeError } from '@mapmetrics/geocoder';
try {
await mapatlas.geocoding.search('Nieuwezijds Voorburgwal 147', { country: 'nl' });
} catch (e) {
if (e instanceof ScopeError) {
// ...
} else if (e instanceof MapAtlasError) {
// catches every other documented failure mode
} else {
throw e;
}
}Prefer e.code / e.name (plain strings) over instanceof if your app might load this package as both ESM and CJS in the same dependency tree — that produces two distinct class objects for the same error, and instanceof won't match across them.
Not every suggestion can be resolved
The gateway returns some rows — injected locality entries, such as the city itself when you type "Amsterdam" — with no retrieve handle. They are still returned so you can show them in a list, but they cannot be turned into coordinates.
suggestion.isRetrievable tells you which is which, and ord is null (never NaN) on those rows:
const results = await session.suggest('Amsterdam', { country: 'nl' });
const usable = results.filter((s) => s.isRetrievable);Calling retrieve() on a non-retrievable suggestion throws NotRetrievableError. retrieveBatch() rejects the whole call if any item is non-retrievable, rather than silently returning fewer places than you asked for. All four SDKs behave identically here.
Endpoint reference
This SDK is a typed wrapper — for the full parameter list, response shape, and gateway-level gotchas each call is protecting you from, see: