Skip to content

Add a Geocoder Search Control to MapLibre GL JS

This example adds a search control to the top right corner of a MapLibre GL map using the maplibre-gl-geocoder plugin. Results are fetched from the Maptoolkit Geocoding API. Try searching for Sillgasse.

Dependencies: maplibre-gl-geocoder

let map = new maplibregl.Map({
      container: "map",
      style: "https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY",
      center: [11.40037, 47.26816],
      zoom: 12,
    });

    map.addControl(
      new MaplibreGeocoder({
        forwardGeocode: async (cfg) => {
          const response = await fetch(
            `https://geocoder.maptoolkit.net/search?q=${encodeURIComponent(cfg.query)}&language=${cfg.language[0]}&api_key=YOUR_API_KEY`
          );
          const result = await response.json();
          return {
            features: result.map((e) => ({
              type: "Feature",
              geometry: { type: "Point", coordinates: [e.lon, e.lat] },
              place_type: ["place"],
              place_name: e.display_name,
              text: e.type,
              properties: e,
              center: [e.lon, e.lat]
            }))
          };
        },
      }, {
        showResultsWhileTyping: true,
        showResultMarkers: false,
        maplibregl: maplibregl
      })
    );
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
  <script src="https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.2.0/dist/maplibre-gl-geocoder.min.js"></script>
  <link href="https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.2.0/dist/maplibre-gl-geocoder.css" rel="stylesheet" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    let map = new maplibregl.Map({
      container: "map",
      style: "https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=YOUR_API_KEY",
      center: [11.40037, 47.26816],
      zoom: 12,
    });

    map.addControl(
      new MaplibreGeocoder({
        forwardGeocode: async (cfg) => {
          const response = await fetch(
            `https://geocoder.maptoolkit.net/search?q=${encodeURIComponent(cfg.query)}&language=${cfg.language[0]}&api_key=YOUR_API_KEY`
          );
          const result = await response.json();
          return {
            features: result.map((e) => ({
              type: "Feature",
              geometry: { type: "Point", coordinates: [e.lon, e.lat] },
              place_type: ["place"],
              place_name: e.display_name,
              text: e.type,
              properties: e,
              center: [e.lon, e.lat]
            }))
          };
        },
      }, {
        showResultsWhileTyping: true,
        showResultMarkers: false,
        maplibregl: maplibregl
      })
    );
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create a map of Innsbruck with a geocoder in the top right corner, using MapLibre GL.

How it works

maplibre-gl-geocoder supplies the search box and expects a forwardGeocode function, so this example supplies one that calls Maptoolkit instead of the plugin’s default provider.

The response is Nominatim-shaped, which explains several details that otherwise look arbitrary. lat and lon come back as strings, not numbers, so they need coercing before a map will accept them. display_name is the full formatted address. boundingbox is [south, north, west, east], also as strings, which is a different order from the [west, south, east, north] that most GeoJSON tooling expects.

The query has to be URL-encoded. encodeURIComponent is not optional: an address with a comma or an ampersand truncates the query without it.

The plugin expects GeoJSON Features, so each result is rebuilt into one, with center set alongside geometry because the control reads that field when flying to a result.

showResultsWhileTyping: true queries the service on each keystroke. Debounce it if request volume matters.

Next steps

Reverse geocoding is the other half, turning a coordinate back into an address, which is what a click-anywhere or drag-a-pin interface needs. The Geocoding API reference covers both directions and the parameters that narrow results by country or feature type.

Most searches are the first step of something else. The Routing API takes the coordinates a result returns, and the Isochrone API turns a found address into the area reachable from it.