Add a search box to a MapLibre map
MapLibre GL JS doesn't come with a search box, but the MapLibre project publishes one: MapLibre GL Geocoder, a control that sits on the map, suggests results as you type, and flies to the one you pick. It doesn't search by itself. You give it a function that calls a geocoding service.
This guide connects it to Mapricot's autocomplete, and adds the opposite direction: click the map to see the address there, from Mapricot's reverse geocoding. Everything goes in one HTML file, with no build step. It was tested with MapLibre GL JS 6.15 and MapLibre GL Geocoder 1.9 in Chromium.
Before you start
You need a browser key with the Places product. Getting started shows how to make one. Add http://localhost:* to its allowed origins, so it works from your computer while you build. The map tiles need no key.
Make the page
Make a folder for the map, and in it a file called index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search the map</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.9.4/dist/maplibre-gl-geocoder.css">
</head>
<body style="margin: 0">
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
import MaplibreGeocoder from "https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.9.4/+esm";
const key = "mapricot_pk_…";
async function mapricot(path, params) {
const response = await fetch(`https://api.mapricot.com${path}?${new URLSearchParams(params)}`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
return response.json();
}
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/light.json",
center: [-122.6793, 45.5189],
zoom: 13,
});
</script>
</body>
</html>
Put your key in place of mapricot_pk_…. The page loads MapLibre, MapLibre GL Geocoder, and their stylesheets from jsDelivr, and draws the map in the map element, which fills the window. Markers and popups explains each part of a page like this. Here, mapricot takes the query parameters as an object and adds them to the URL.
Open the page
Serve the folder with a web server, rather than opening the file directly. Opened from a file, the map stays blank with Worker failed to load, since browsers don't let a page from a file start MapLibre's web worker. Such a page also has no origin, so the API would refuse the key with 403 and "Key is not allowed from this origin." Any web server works. In the folder, run one of these:
npx serve
python3 -m http.server
npx serve needs Node.js, and serves the page at http://localhost:3000. Python's serves it at http://localhost:8000. Open the address to see a map of Portland, and reload it after each change below.
Connect the geocoder to Mapricot
The geocoder calls forwardGeocode with what's been typed, and expects GeoJSON features back, in the format Mapbox's geocoder used: each with a point, a place_name to show, and a shorter text. This one asks Mapricot's autocomplete and turns each result into a feature. Add it at the end of the script:
const geocoderApi = {
async forwardGeocode({ query, proximity, limit }) {
const params = { q: query, limit };
if (proximity) params.focus = `${proximity[1].toFixed(6)},${proximity[0].toFixed(6)}`;
const { results } = await mapricot("/v1/autocomplete", params);
return {
type: "FeatureCollection",
features: results.map((result) => ({
type: "Feature",
geometry: { type: "Point", coordinates: [result.lon, result.lat] },
properties: result,
text: result.name,
place_name: result.label,
place_type: [result.type],
})),
};
},
};
proximity is the map's center as [longitude, latitude], once the map is zoomed in past level 9. Passed to Mapricot as focus, it ranks results near what's on screen first, so "stumptown" in Portland finds the coffee shops downtown. Without it, the first results are a Stumptown Coffee in Kyoto and a museum in Montana. Mapricot takes focus as lat,lon, the other way around, and with at most 10 decimal places, hence toFixed(6).
The whole Mapricot result goes in properties, so its address parts and ID are there when one is picked.
This is the request it sends for "stumptown". Type in the box to send your own:
curl "https://api.mapricot.com/v1/autocomplete?q=stumptown&focus=45.51886,-122.67932&limit=5" \
-H "Authorization: Bearer $MAPRICOT_KEY"{ "attribution": [ { "source": "openstreetmap", "text": "© OpenStreetMap contributors", "url": "https://www.openstreetmap.org/copyright" } ], "results": [ { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzExOTg2MjI2NjYw", "type": "venue", "name": "Stumptown Coffee Roasters", "label": "Stumptown Coffee Roasters, Portland, OR, USA", "lat": 45.521399, "lon": -122.682775, "address": { "houseNumber": "1140", "street": "SW Washington Street", "postalCode": "97205", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 391, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTp3YXkvMjQzMDg1NDQz", "type": "venue", "name": "Stumptown Coffee Roasters", "label": "Stumptown Coffee Roasters, Portland, OR, USA", "lat": 45.52174, "lon": -122.673291, "address": { "houseNumber": "128", "street": "SW 3rd Avenue", "postalCode": "97206", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 569, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzEyNTEzMDAxOTYy", "type": "venue", "name": "Stumptown Otaku", "label": "Stumptown Otaku, Portland, OR, USA", "lat": 45.521667, "lon": -122.672808, "address": { "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 596, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTp3YXkvMzA2MzUwNDQw", "type": "venue", "name": "Stumptown Test Prep", "label": "Stumptown Test Prep, Portland, OR, USA", "lat": 45.478915, "lon": -122.67351, "address": { "houseNumber": "6222", "street": "South Virginia Avenue", "postalCode": "97239", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 4470, "source": "openstreetmap" }, { "id": "mp_b3BlbnN0cmVldG1hcDp2ZW51ZTpub2RlLzMyNDE5MzY3MA", "type": "venue", "name": "Stumptown", "label": "Stumptown, Portland, OR, USA", "lat": 45.505425, "lon": -122.615561, "address": { "houseNumber": "4525", "street": "SE Division Street", "postalCode": "97206", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 5194, "source": "openstreetmap" } ]}Add the search box
Add the control to the map, at the end of the script:
function escape(text) {
return text.replace(/[&<>"]/g, (character) => `&#${character.charCodeAt(0)};`);
}
const geocoder = new MaplibreGeocoder(geocoderApi, {
maplibregl,
showResultsWhileTyping: true,
placeholder: "Search for an address or place",
marker: { color: "#fd7e14" },
render: (feature) =>
`<div class="maplibregl-ctrl-geocoder--result-title">${escape(feature.text)}</div>` +
`<div class="maplibregl-ctrl-geocoder--result-address">${escape(feature.place_name.replace(`${feature.text}, `, ""))}</div>`,
});
map.addControl(geocoder, "top-left");
maplibregllets the control add a marker at the result you pick, in the colormarkersets.showResultsWhileTypingsends a request as people type, 200 milliseconds after they stop, so typing a word costs one or two requests rather than one per letter. Without it, the geocoder only searches on Enter.renderdraws each suggestion: its name, and under it the rest of its label, such as "Portland, OR, USA". The geocoder's ownrenderputs names into the page as HTML, and place names come from map data that anyone can edit, so this one escapes them first.
Reload the page and type an address or a place. Picking a result flies the map to it. To do more, such as filling in a form, listen for the result. Add this at the end of the script, and the picked place's address shows in the browser's console:
geocoder.on("result", ({ result }) => console.log(result.properties.address));
Show the address at a clicked point
Reverse geocoding turns a point into an address. Ask for the address nearest wherever the map is clicked, and show it in a popup. Add this at the end of the script:
map.on("click", async ({ lngLat }) => {
const { lat, lng } = lngLat.wrap();
const { results } = await mapricot("/v1/reverse", { lat: lat.toFixed(6), lon: lng.toFixed(6), limit: 1 });
new maplibregl.Popup()
.setLngLat(lngLat)
.setText(results[0]?.label ?? "No address here")
.addTo(map);
});
wrap keeps the longitude between -180 and 180 after the map has been panned around the world, and toFixed(6) rounds the coordinates, which MapLibre gives to about 14 decimal places. setText shows the label as plain text. Away from any address, such as in the middle of a lake, the result can be a street, a town, or nothing at all. Click the map to try it:
curl "https://api.mapricot.com/v1/reverse?lat=45.5217&lon=-122.6733&limit=1" \
-H "Authorization: Bearer $MAPRICOT_KEY"{ "attribution": [ { "source": "openaddresses", "text": "OpenAddresses", "url": "https://openaddresses.io" } ], "results": [ { "id": "mp_b3BlbmFkZHJlc3NlczphZGRyZXNzOnVzL29yL211bHRub21haDpkZjdlYzM3YWJiYTJiNWM0", "type": "address", "name": "128 SW 3rd Avenue", "label": "128 SW 3rd Avenue, Portland, OR, USA", "lat": 45.521736, "lon": -122.673276, "address": { "houseNumber": "128", "street": "SW 3rd Avenue", "postalCode": "97204", "locality": "Portland", "region": "Oregon", "regionCode": "OR", "country": "United States", "countryCode": "US" }, "distanceMeters": 4, "source": "openaddresses" } ]}The whole page
Here's index.html with everything above, ready to copy. Put your key in place of mapricot_pk_…:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search the map</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.9.4/dist/maplibre-gl-geocoder.css">
</head>
<body style="margin: 0">
<div id="map" style="height: 100vh"></div>
<script type="module">
import * as maplibregl from "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm";
import MaplibreGeocoder from "https://cdn.jsdelivr.net/npm/@maplibre/maplibre-gl-geocoder@1.9.4/+esm";
const key = "mapricot_pk_…";
async function mapricot(path, params) {
const response = await fetch(`https://api.mapricot.com${path}?${new URLSearchParams(params)}`, {
headers: { Authorization: `Bearer ${key}` },
});
if (!response.ok) throw new Error(`Mapricot returned ${response.status}`);
return response.json();
}
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.mapricot.com/styles/light.json",
center: [-122.6793, 45.5189],
zoom: 13,
});
const geocoderApi = {
async forwardGeocode({ query, proximity, limit }) {
const params = { q: query, limit };
if (proximity) params.focus = `${proximity[1].toFixed(6)},${proximity[0].toFixed(6)}`;
const { results } = await mapricot("/v1/autocomplete", params);
return {
type: "FeatureCollection",
features: results.map((result) => ({
type: "Feature",
geometry: { type: "Point", coordinates: [result.lon, result.lat] },
properties: result,
text: result.name,
place_name: result.label,
place_type: [result.type],
})),
};
},
};
function escape(text) {
return text.replace(/[&<>"]/g, (character) => `&#${character.charCodeAt(0)};`);
}
const geocoder = new MaplibreGeocoder(geocoderApi, {
maplibregl,
showResultsWhileTyping: true,
placeholder: "Search for an address or place",
marker: { color: "#fd7e14" },
render: (feature) =>
`<div class="maplibregl-ctrl-geocoder--result-title">${escape(feature.text)}</div>` +
`<div class="maplibregl-ctrl-geocoder--result-address">${escape(feature.place_name.replace(`${feature.text}, `, ""))}</div>`,
});
map.addControl(geocoder, "top-left");
geocoder.on("result", ({ result }) => console.log(result.properties.address));
map.on("click", async ({ lngLat }) => {
const { lat, lng } = lngLat.wrap();
const { results } = await mapricot("/v1/reverse", { lat: lat.toFixed(6), lon: lng.toFixed(6), limit: 1 });
new maplibregl.Popup()
.setLngLat(lngLat)
.setText(results[0]?.label ?? "No address here")
.addTo(map);
});
</script>
</body>
</html>
Credits
The map style credits OpenStreetMap. Results also list their own sources in attribution, such as OpenAddresses for an address. When your map shows them, add those credits too, for example with the map's customAttribution option: new maplibregl.Map({ …, attributionControl: { customAttribution: "OpenAddresses" } }).
Questions
Why don't any suggestions show? Open the browser's console. Mapricot returned 403 means the API refused the key: check that the page is served from an origin the key allows, such as http://localhost:*, and not opened as a file, and that the key has the Places product.
Why not use the geocoder's reverseGeocode option? It handles coordinates typed into the search box, such as 45.52, -122.68, which is useful for some apps. It isn't needed for clicking the map. To support it, add a reverseGeocode function to geocoderApi that calls /v1/reverse with query[1] as lat and query[0] as lon, and set reverseGeocode: true.
Can I put the search box outside the map? Yes. Instead of map.addControl, call geocoder.addTo("#search") with a selector for any element on the page. It isn't connected to the map then, so move the map yourself in its result event, for example with map.flyTo({ center: result.geometry.coordinates, zoom: 16 }).
How do I limit results to one country? Add country: "US" to params, or several codes as "US,CA".
How many requests does this use? One for each pause in typing, and one for each click on the map. Map tiles are free and don't count. To make a standalone address form instead, address autocomplete builds one in plain JavaScript.