Skip to content
Make with

Geocoding

Address search for MapLibre maps: drop-in search controls, the geocoding engines behind them, and hosted geocoding APIs, compared on price, features, and terms.

Try it

A live search box: maplibre-gl-geocoder on a MapLibre map, searching OpenStreetMap through Photon, komoot's open-source geocoder, on its free public instance.

Loading the map…

Demo code
// The live search box on the Geocoding page: maplibre-gl-geocoder on a
// MapLibre map, searching OpenStreetMap through Photon. The page loads this
// module only when the map scrolls into view.
import MaplibreGeocoder from "@maplibre/maplibre-gl-geocoder";
import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css";
import type { FitBoundsOptions, FlyToOptions } from "maplibre-gl";
import { maplibregl } from "./maplibre";
import { photonGeocoderApi } from "./photon";

// The plugin hands these to fitBounds for results with an area and to flyTo
// for points, so they may use either one's options. Without the cap, a
// building-sized result would zoom in to about 20.
const flyTo: FlyToOptions & FitBoundsOptions = { maxZoom: 16 };

export function mountGeocoder(container: HTMLElement): void {
  const map = new maplibregl.Map({
    container,
    style: "https://tiles.openfreemap.org/styles/liberty",
    center: [10, 30],
    zoom: 1.4,
    cooperativeGestures: true,
    attributionControl: {
      compact: true,
      customAttribution: 'Search: <a href="https://photon.komoot.io">Photon</a> by komoot',
    },
  });

  map.addControl(
    new MaplibreGeocoder(photonGeocoderApi, {
      maplibregl,
      placeholder: "Search a place, like Kyoto",
      showResultsWhileTyping: true,
      minLength: 3,
      debounceSearch: 300,
      flyTo,
    }),
    "top-left",
  );
}

The Photon adapter turns Photon's results into the plugin's format:

// maplibre-gl-geocoder backed by Photon (https://photon.komoot.io), komoot's
// open-source geocoder for OpenStreetMap data. Its public instance allows use
// "as long as the number of requests stay in a reasonable limit" and needs no
// key; the map must credit OpenStreetMap, which the basemap already does.
import type {
  CarmenGeojsonFeature,
  MaplibreGeocoderApi,
  MaplibreGeocoderApiConfig,
  MaplibreGeocoderFeatureResults,
} from "@maplibre/maplibre-gl-geocoder";

const PHOTON = "https://photon.komoot.io";
// The public instance answers HTTP 400 for any other language.
const LANGUAGES = ["en", "de", "fr"];

interface PhotonProperties {
  osm_type?: string;
  osm_id?: number;
  osm_value?: string;
  type?: string;
  name?: string;
  street?: string;
  housenumber?: string;
  postcode?: string;
  city?: string;
  state?: string;
  country?: string;
  extent?: [number, number, number, number]; // [west, north, east, south]
}

interface PhotonFeature {
  geometry: { type: "Point"; coordinates: [number, number] };
  properties: PhotonProperties;
}

// Photon's extent is [west, north, east, south]; Carmen's bbox is [west, south, east, north].
function toBbox([x1, y1, x2, y2]: [number, number, number, number]): [
  number,
  number,
  number,
  number,
] {
  return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)];
}

function toCarmen(feature: PhotonFeature): CarmenGeojsonFeature {
  const p = feature.properties;
  const street = [p.street, p.housenumber].filter(Boolean).join(" ");
  const text = p.name || street || p.city || p.state || p.country || "";
  // "Station", "City": tells apart places that share a name.
  const kind = p.osm_value && p.osm_value !== "yes" ? p.osm_value.replaceAll("_", " ") : "";
  const label = kind.charAt(0).toUpperCase() + kind.slice(1);
  const parts = [
    text,
    label,
    street,
    [p.postcode, p.city].filter(Boolean).join(" "),
    p.state,
    p.country,
  ];
  return {
    type: "Feature",
    id: `${p.osm_type}${p.osm_id}`,
    geometry: feature.geometry,
    properties: { ...p },
    text,
    place_name: [...new Set(parts.filter(Boolean))].join(", "),
    place_type: [p.type || "place"],
    center: feature.geometry.coordinates,
    ...(p.extent && { bbox: toBbox(p.extent) }),
  };
}

async function photon(
  path: string,
  params: URLSearchParams,
): Promise<MaplibreGeocoderFeatureResults> {
  const response = await fetch(`${PHOTON}${path}?${params}`);
  if (!response.ok) throw new Error(`Photon ${path}: HTTP ${response.status}`);
  const json = (await response.json()) as { features: PhotonFeature[] };
  return { type: "FeatureCollection", features: json.features.map(toCarmen) };
}

// The plugin's types say comma-separated strings, but it passes arrays at runtime.
const list = (value: string | string[] | undefined): string[] =>
  (Array.isArray(value) ? value : (value?.split(",") ?? []))
    .map((item) => item.trim())
    .filter(Boolean);

function withLanguage(params: URLSearchParams, config: MaplibreGeocoderApiConfig) {
  const code = list(config.language)[0]?.slice(0, 2).toLowerCase();
  if (code && LANGUAGES.includes(code)) params.set("lang", code);
  return params;
}

export const photonGeocoderApi: MaplibreGeocoderApi = {
  forwardGeocode(config) {
    const params = new URLSearchParams({
      q: String(config.query),
      limit: String(config.limit ?? 5),
    });
    if (config.proximity) {
      params.set("lon", String(config.proximity[0]));
      params.set("lat", String(config.proximity[1]));
    }
    if (config.bbox) params.set("bbox", config.bbox.join(","));
    for (const country of list(config.countries)) params.append("countrycode", country);
    return photon("/api/", withLanguage(params, config));
  },
  reverseGeocode(config) {
    const [lon, lat] = config.query as number[];
    const params = new URLSearchParams({ lon: String(lon), lat: String(lat), limit: "1" });
    return photon("/reverse", withLanguage(params, config));
  },
};

Search controls

Drop-in address search components for MapLibre GL JS maps.

Geocoding engines

Self-hosted backends for address search and reverse geocoding.

Hosted geocoding APIs

Geocoding services you can call from a MapLibre map without running an engine.

Compare the options

Free tiers, features, and what the terms let you do with the results. Storing matters more than it seems: saving geocoded addresses in your own database is often restricted or billed separately.

Three of these can't be shown on a MapLibre map

Google's terms forbid using Geocoding API results "in conjunction with a non-Google map" (Service Specific Terms 6.2). Mapbox allows its results "only in conjunction with a Mapbox map" (Geocoding API docs). Azure Maps forbids displaying results "on any third-party content or geographical map database" (Product Terms).

Hosted APIs

Hosted geocoding APIs compared by free tier, features, storage terms, data, and license
NameFree tierNeeds a keyAutocompleteReverseStoring resultsDataOpen source
MapTiler Geocoding ↗100,000 API requests/month, shared across MapTiler APIs (non-commercial use only)YesYesYesAllowedMixedNo
Mapbox Geocoding ↗100,000 requests/month (Temporary geocoding only; Permanent geocoding has no free tier)YesYesYesRestricted1Mixed?
Stadia Maps Geocoding ↗200,000 credits/month, non-commercial only (a geocode costs 20 credits, a v2 autocomplete 1)YesYesYesRestricted2MixedYes
Geoapify Geocoding ↗3,000 credits/day (1 credit per geocoding request), commercial use allowed with attributionYesYesYesAllowedMixed?
OpenCage ↗None for production; free trial of 2,500 requests/day for testing onlyYesYesYesAllowedMixed?
HERE Geocoding ↗30,000 geocode and reverse geocode requests/monthYesYesYesRestricted3ProprietaryNo
Esri Geocoding ↗20,000 geocodes/month, not stored (stored geocodes have no free tier)YesYesYesRestricted4MixedNo
TomTom Geocoding ↗20,000 geocoding and 20,000 reverse geocoding requests/monthYesYesYesRestricted5MixedNo
Amazon Location Service ↗None standing; a 3-month free trial includes 20,000 geocode or reverse geocode requests and 10,000 autocomplete requests per monthYesYesYesRestricted6ProprietaryNo
Azure Maps ↗5,000 Search transactions/month (10 autocomplete requests count as 1 transaction)YesYesYesRestricted7MixedNo
Google Geocoding ↗10,000 requests/month (billing must be enabled)YesYesYesRestricted8ProprietaryNo
Jawg Places ↗5,000 place searches/month, non-commercial use onlyYesYesYesForbidden9MixedYes
Maptoolkit Geocoding ↗500 API requests/month, shared with Maptoolkit's other APIs (Test & Develop plan, hard limit)Yes?YesAllowedMixedYes

1Temporary results may not be cached; storing needs Permanent geocoding, a paid mode with a card on file.

2Permanent storage needs a Standard plan or higher; below that, only client-side caching for as long as the HTTP cache headers allow, or 7 days without one (Terms, section 8).

3Results may not be cached or stored outside the platform for more than 30 days (Platform Terms 6.4(j)); permanent geocoding is sold separately.

4Storing results is contractually prohibited unless requested with forStorage=true, which is billed as stored geocodes.

5Storing results is prohibited; clients may cache them only for the max-age in the response's cache headers (Terms 11.4).

6Results may be stored only when the request sets IntendedUse to Storage, billed at a higher rate (AWS Service Terms 82.4).

7Caching only to reduce latency, for at most 6 months, though geocodes may be kept while the Azure account is active (Product Terms, Azure Maps).

8Latitude and longitude may be cached for up to 30 days; longer caching only per end user, for that user's own feature (Service Specific Terms 6.3).

9The terms (section 5.5) name storing Transactions results as a prohibited way of reducing the fees you owe.

From each provider's pricing, docs, and terms, checked September 2026. "?" means the provider's pages don't say. Prices and terms change, so check them before you commit.

Engines you run yourself

Self-hosted geocoding engines compared by features, storage terms, and data
NameCostAutocompleteReverseStoring resultsDataOpen source
NominatimSelf-hosted: unlimited (the public OSM server allows at most 1 request/second)NoYesAllowedOpenStreetMapYes
PhotonSelf-hosted: unlimited (public instance photon.komoot.io: fair use, heavy use throttled)YesYesAllowedOpenStreetMapYes
PeliasSelf-hosted: unlimitedYesYesAllowedMixedYes

From each project's docs and data licenses, checked September 2026.

Know of another geocoding tool?

Suggest search controls, geocoding engines, or hosted geocoding services.