Get an Address from a Clicked Point in Leaflet
Reverse geocoding turns a coordinate into an address. A click-anywhere map, a drag-the-pin
confirmation step and a “use my location” button all need it, because none of them start from
something the user typed. This example listens for clicks on a Leaflet map, sends the
coordinate to the reverse endpoint of the Maptoolkit Geocoding API and shows the address in a
popup at that point. No plugin is needed: it is one fetch.
const API_KEY = "YOUR_API_KEY";
const map = L.map("map", { zoomControl: false }).setView([48.2082, 16.3722], 16);
L.control.zoom({ position: "topright" }).addTo(map);
L.tileLayer(`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=${API_KEY}`, {
maxZoom: 18,
attribution:
"© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
"© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
}).addTo(map);
const hint = L.control({ position: "topleft" });
hint.onAdd = () => L.DomUtil.create("div", "hint");
hint.addTo(map);
hint.getContainer().textContent = "Click anywhere on the map";
const marker = L.marker([0, 0]);
let latest = 0;
// Two lines of plain text, so an address can never inject markup into the page.
function showAddress(title, detail) {
const div = L.DomUtil.create("div", "address");
L.DomUtil.create("strong", "", div).textContent = title;
if (detail) L.DomUtil.create("span", "", div).textContent = detail;
marker.setPopupContent(div);
}
map.on("click", async (event) => {
const { lat, lng } = event.latlng;
const request = ++latest;
marker.setLatLng(event.latlng).addTo(map).bindPopup("", { maxWidth: 260 }).openPopup();
showAddress("Looking up...");
const url = new URL("https://geocoder.maptoolkit.net/reverse");
url.searchParams.set("lat", lat);
// The reverse endpoint takes lon, not lng.
url.searchParams.set("lon", lng);
url.searchParams.set("language", "en");
url.searchParams.set("api_key", API_KEY);
try {
const result = await (await fetch(url)).json();
// A later click has already sent its own request, so this answer is stale.
if (request !== latest) return;
const address = result.address || {};
const street = [address.road, address.house_number].filter(Boolean).join(" ");
const place = [address.postcode, address.city || address.town || address.village].filter(Boolean).join(" ");
// Not every point has a street address: fall back through the fields that are present.
const title = street || result.name || address.suburb || address.neighbourhood ||
(result.display_name || "").split(",")[0] || "This location";
showAddress(title, [place, address.country].filter(Boolean).join(", "));
} catch {
if (request === latest) showAddress("No address found here.");
}
});<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
.hint {
padding: 8px 12px; background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(20, 30, 60, 0.18);
font: 13px/1.4 system-ui, sans-serif; color: #1f2430;
}
.address { font: 13px/1.5 system-ui, sans-serif; color: #1f2430; }
.address strong { display: block; font-size: 14px; }
.address span { color: #6b7185; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = "YOUR_API_KEY";
const map = L.map("map", { zoomControl: false }).setView([48.2082, 16.3722], 16);
L.control.zoom({ position: "topright" }).addTo(map);
L.tileLayer(`https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=${API_KEY}`, {
maxZoom: 18,
attribution:
"© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
"© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
}).addTo(map);
const hint = L.control({ position: "topleft" });
hint.onAdd = () => L.DomUtil.create("div", "hint");
hint.addTo(map);
hint.getContainer().textContent = "Click anywhere on the map";
const marker = L.marker([0, 0]);
let latest = 0;
// Two lines of plain text, so an address can never inject markup into the page.
function showAddress(title, detail) {
const div = L.DomUtil.create("div", "address");
L.DomUtil.create("strong", "", div).textContent = title;
if (detail) L.DomUtil.create("span", "", div).textContent = detail;
marker.setPopupContent(div);
}
map.on("click", async (event) => {
const { lat, lng } = event.latlng;
const request = ++latest;
marker.setLatLng(event.latlng).addTo(map).bindPopup("", { maxWidth: 260 }).openPopup();
showAddress("Looking up...");
const url = new URL("https://geocoder.maptoolkit.net/reverse");
url.searchParams.set("lat", lat);
// The reverse endpoint takes lon, not lng.
url.searchParams.set("lon", lng);
url.searchParams.set("language", "en");
url.searchParams.set("api_key", API_KEY);
try {
const result = await (await fetch(url)).json();
// A later click has already sent its own request, so this answer is stale.
if (request !== latest) return;
const address = result.address || {};
const street = [address.road, address.house_number].filter(Boolean).join(" ");
const place = [address.postcode, address.city || address.town || address.village].filter(Boolean).join(" ");
// Not every point has a street address: fall back through the fields that are present.
const title = street || result.name || address.suburb || address.neighbourhood ||
(result.display_name || "").split(",")[0] || "This location";
showAddress(title, [place, address.country].filter(Boolean).join(", "));
} catch {
if (request === latest) showAddress("No address found here.");
}
});
</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
Leaflet’s click event carries latlng, the position under the cursor. The handler moves a
marker there, opens its popup and asks the Geocoding API reverse endpoint
what is at that coordinate.
The parameter is lon, not lng. The reverse endpoint takes lat and lon. A lng
parameter is not rejected, it is ignored, and the answer then has nothing to do with the click.
Set language. It defaults to de, so an English page gets “Wien” and “Österreich”
unless it asks for en.
The nearest feature is not always an address. A click on a square, a park or a shop returns
that feature, sometimes with no road at all. The popup reads address.road and
address.house_number first and then falls back through name, suburb and the first part of
display_name, because any single field is empty somewhere.
Ignore stale answers. Clicks come faster than the network answers, and responses do not
arrive in order. latest counts the requests, and a response only writes to the popup if no
later click has been made in the meantime.
Text, not HTML. Leaflet popups accept HTML strings. The content here is built with
L.DomUtil.create() and textContent instead, so a feature name from the map data is shown as
text and never parsed as markup.
boundingbox in the response is [minLat, maxLat, minLng, maxLng]. To frame the map on a
result with fitBounds(), reorder it to [[minLat, minLng], [maxLat, maxLng]].
Next steps
To type an address instead of clicking it, add the geocoder search control. From a chosen point, the route planner gives directions to it, and an isochrone shows what is reachable around it.
The same example in Maptoolkit Maps JS is Get an Address from a Clicked Point.