Add a Geocoder Search Control to MapLibre GL JS
On this page
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.
The plugin only supplies the search box. The Geocoding API itself is a plain HTTP request and
needs no library: Get an Address from a Clicked Point
calls it with fetch alone.
import * as maplibregl from "maplibre-gl";
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
let map = new maplibregl.Map({
container: "map",
attributionControl: { compact: false },
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
})
);
map.addControl(new LogoControl({ variant: "com" }));<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- MapLibre GL JS 6 is an ES module. The import map tells the browser
where to load "maplibre-gl" from. -->
<script type="importmap">
{
"imports": {
"maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.mjs"
}
}
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.16.0/dist/maplibre-gl.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.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 type="module">
import * as maplibregl from "maplibre-gl";
import { LogoControl } from "https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-logo-control@1.1.2/dist/maplibre-logo-control.js";
let map = new maplibregl.Map({
container: "map",
attributionControl: { compact: false },
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
})
);
map.addControl(new LogoControl({ variant: "com" }));
</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.
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.