Put a searched address on a map with MapLibre
About 15 minutes. Updated 11 September 2026.
Address autocomplete that drops a pin on a MapLibre map as soon as an address is picked, with OpenFreeMap tiles so there is no second API key to manage, a pin that follows each pick, and a dark mode that does not leave a bright rectangle on the page.
Every address the API returns already carries its G-NAF coordinates, so putting it on a map is one line after the pick: no geocoding call, no second request. MapLibre is the map, OpenFreeMap serves the tiles, and neither wants a key of its own, so the only credential on the page is the public key for the address search.
01Install MapLibre
One package, which brings its own stylesheet. If you would rather not bundle, the same two files are served from a CDN; the MapLibre site shows the tags.
npm install maplibre-gl02The markup
The search input and list from the autocomplete guide, plus a box for the map. Give the box its height in the page: a map that sizes itself makes the page jump when the tiles arrive.
<label for="address">Address</label>
<input id="address" type="search" autocomplete="off" placeholder="145 sydney road">
<ul id="suggestions" hidden></ul>
<!-- A fixed height, set here rather than by the map, so the page does not
jump when the tiles arrive. -->
<div id="map" style="height: 360px; border-radius: 8px; overflow: hidden"></div>
<script type="module" src="/map-search.js"></script>03Search, pick, fly
The map opens on Australia. Each result from /v1/addresses is a resolved address with lat and lng, so picking one moves the single pin and flies the map to it. The pin is created once and moved, so the second pick does not leave the first one behind.
// map-search.js
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
const KEY = "lc_live_..."; // a public key, with this site under its allowed sites
const API = "https://api.locio.com.au";
// Australia, zoomed out, until there is somewhere to look.
const map = new maplibregl.Map({
container: "map",
style: "https://tiles.openfreemap.org/styles/bright", // no key, no account, no cap
center: [134.5, -27.5],
zoom: 3.4,
attributionControl: { compact: true },
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");
// One pin, moved rather than replaced, so a second pick does not leave the
// first one behind.
const pin = new maplibregl.Marker({ color: "#18181b" });
const input = document.querySelector("#address");
const list = document.querySelector("#suggestions");
let timer;
let inflight;
input.addEventListener("input", () => {
clearTimeout(timer);
const q = input.value.trim();
if (q.length < 3) {
list.hidden = true;
return;
}
timer = setTimeout(() => search(q), 200); // one request per pause, not per keystroke
});
async function search(q) {
inflight?.abort();
inflight = new AbortController();
const url = new URL(`${API}/v1/addresses`);
url.searchParams.set("q", q);
url.searchParams.set("limit", "8");
try {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
signal: inflight.signal,
});
if (!res.ok) return;
const { data } = await res.json();
list.replaceChildren(
...data.map((address) => {
const item = document.createElement("li");
const button = document.createElement("button");
button.type = "button";
button.textContent = address.formatted;
button.addEventListener("click", () => show(address));
item.append(button);
return item;
}),
);
list.hidden = data.length === 0;
} catch (err) {
if (err.name !== "AbortError") console.error(err);
}
}
function show(address) {
input.value = address.formatted;
list.hidden = true;
// Every result is already resolved, so the coordinates are on it: no
// geocoding call, no second request.
const at = [address.lng, address.lat];
pin.setLngLat(at).setPopup(new maplibregl.Popup({ offset: 24 }).setText(address.formatted)).addTo(map);
map.flyTo({ center: at, zoom: 17, speed: 1.6 });
}04Dark mode
OpenFreeMap has no dark style, so a dark page would otherwise carry a bright rectangle. Invert the map's canvas and rotate the hue back; the pin and the popup are ordinary elements and are left alone. This is exactly what the demo on our front page does.
/* Dark mode. OpenFreeMap has no dark style, so invert the canvas and turn
the hue back, which keeps parks green and water blue. The pin and the
popup are DOM, not canvas, so they are untouched. */
@media (prefers-color-scheme: dark) {
#map .maplibregl-canvas {
filter: invert(1) hue-rotate(180deg);
}
#map .maplibregl-popup-content {
color: #27272a; /* the popup is white either way */
}
}05More than one address
A page showing every store, or every row of a cleaned spreadsheet, wants a layer rather than a marker each: MapLibre draws a GeoJSON source itself and stays smooth at a thousand points.
// Several addresses at once: a GeoJSON source and a circle layer, which
// the map draws itself, rather than a marker element per address.
map.on("load", () => {
map.addSource("addresses", {
type: "geojson",
data: {
type: "FeatureCollection",
features: addresses.map((a) => ({
type: "Feature",
geometry: { type: "Point", coordinates: [a.lng, a.lat] },
properties: { label: a.formatted, pid: a.gnaf_pid },
})),
},
});
map.addLayer({
id: "addresses",
type: "circle",
source: "addresses",
paint: { "circle-radius": 6, "circle-color": "#18181b", "circle-stroke-width": 2, "circle-stroke-color": "#fff" },
});
});Attribution
MapLibre's attribution control, kept in the code above, credits OpenStreetMap and OpenFreeMap, which the map data's licence requires. The address data's own credit, G-NAF under CC BY 4.0, belongs wherever you show what the API returned; the licensing page has the wording.
Related
- Address autocomplete in plain JavaScript
- Address autocomplete in React
- Address autocomplete in Vue
- Stand up a React site with Australian address search
- Validate and geocode Australian addresses in PHP
- Use Locio from Claude Code, Claude Desktop and Cursor
- Give an agent address tools with the Anthropic API
- Clean a spreadsheet of addresses with an agent
- The API reference, with every call in curl, Node, Python, Ruby, Go and PHP.