Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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

bash
npm install @mapmetrics/geocoder

Quickstart

ts
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

ts
new MapAtlas({ token: 'YOUR_API_KEY' });
// or, if you rotate/refresh tokens yourself:
new MapAtlas({ getToken: async () => await fetchFreshToken() });
OptionDefaultNotes
tokenExactly one of token / getToken is required.
getTokenCalled fresh on every request — never cached by this package.
baseUrlhttps://gateway.mapmetrics-atlas.netOverride for testing or a self-hosted gateway.
tier'v2''osm' routes to the free, rate-limited OpenStreetMap-only tier.
debounceMs150Debounces 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.

bash
npm install react react-dom   # if your app doesn't already have them
tsx
import { 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():

ts
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 no Origin header. Native/server clients can never satisfy this — see Choosing a key for native apps.
  • OriginNotAllowedError — request's Origin isn't on the key's allow-list.
  • QuotaExceededError — OSM tier cap exhausted; carries selfHostUrl.
  • TierUnsupportedError — thrown by createSession() on the osm tier and on selfHosted() clients, and by autocomplete() on the v2 tier.
  • NetworkError — the request never completed, or the response wasn't parseable JSON.
ts
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:

ts
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: