Address autocomplete API
Address suggestions, free on every keystroke.
Complete addresses and places while the last word is still being typed. Autocomplete calls spend no credits; retrieving the place a user selects costs one.
50,000 free credits a month. No credit card.
Interactive demo
Find a place. See the data.
Search an address or pick a point on the map.
Click the map to find an address
Type an address, then select a suggestion to see its coordinates. Use the arrow keys and Enter, or click a result. No key needed for this demo; shared per-IP demo limits apply.
From a partial address to a selected location.
1. Suggest while typing
Send the partial text to /v1/autocomplete. Suggestions contain labels and opaque IDs, not coordinates. These calls spend no credits.
2. Retrieve the selection
Send only the selected ID to /v1/retrieve. A successful retrieval costs one credit. Do not retrieve every suggestion as it arrives.
3. Use the coordinates
Place a marker, fill your address form or save the location. Let the user confirm the address: coverage varies, and a suggestion is not proof of deliverability or a verified unit.
Plain JavaScript · no SDK
Add address autocomplete to an HTML form.
Download a complete HTML example with keyboard navigation, a 500 ms debounce, cancelled stale requests and readable errors. It calls the public API directly; the demo above uses our server-side proxy instead.
- Create a browser key, not a
secret key. Add the exact origin where you will serve the file, including its port:
http://localhost:8765for the command below. - Save the file as
address-autocomplete.htmland replace theapiKeyvalue. Never put a secret server key in this file. - Run the command in that folder and open
http://localhost:8765/address-autocomplete.html. Do not open it withfile://: browser keys need an allowed HTTP origin.
python3 -m http.server 8765 --bind localhost A browser key is visible in page source. Origin restrictions stop other websites using it from their visitors' browsers; they are not a per-key spending cap. To keep a key private, proxy requests through your own backend. Browser-key security →
Complete browser example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<title>Address autocomplete with OpenGeocoding</title>
<style>
body {
font:
1rem/1.5 system-ui,
sans-serif;
max-width: 36rem;
margin: 3rem auto;
padding: 0 1rem;
}
input,
button {
font: inherit;
padding: 0.75rem;
}
input {
box-sizing: border-box;
width: 100%;
}
ul {
list-style: none;
padding: 0;
border: 1px solid;
}
button {
width: 100%;
text-align: left;
border: 0;
background: white;
color: black;
cursor: pointer;
}
button[aria-selected='true'] {
background: #dbeafe;
}
output {
display: block;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
</style>
</head>
<body>
<h1>Find an address</h1>
<p>Suggestions use no credits. Selecting one retrieves its coordinates for one credit.</p>
<label for="address">Address</label>
<input
id="address"
role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="suggestions"
aria-describedby="status"
autocomplete="off"
spellcheck="false"
/>
<ul id="suggestions" role="listbox" aria-label="Address suggestions" hidden></ul>
<p id="status" role="status">
Type at least two characters. Use the arrow keys and Enter to select.
</p>
<output id="location" aria-label="Selected location"></output>
<p>
Data: <a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</a> and
other open-data contributors.
<a href="https://opengeocoding.com/legal/attributions">Attributions and licences</a>.
</p>
<script type="module">
const apiKey = 'og_YOUR_BROWSER_KEY';
const input = document.querySelector('#address');
const list = document.querySelector('#suggestions');
const status = document.querySelector('#status');
const output = document.querySelector('#location');
let suggestions = [];
let active = -1;
let sequence = 0;
let timer;
let controller;
let retryAt = 0;
let retrieving = false;
function cancel() {
clearTimeout(timer);
controller?.abort();
retrieving = false;
controller = new AbortController();
return ++sequence;
}
function close() {
suggestions = [];
active = -1;
list.replaceChildren();
list.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
}
async function call(endpoint, body) {
if (!/^og_[A-Za-z0-9_-]{32}$/.test(apiKey)) {
throw new Error(
'Replace og_YOUR_BROWSER_KEY with a browser key restricted to this origin.'
);
}
if (Date.now() < retryAt) {
throw new Error(
`Wait ${Math.ceil((retryAt - Date.now()) / 1000)} seconds before trying again.`
);
}
const response = await fetch(`https://api.opengeocoding.com/v1/${endpoint}`, {
method: 'POST',
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(10000)]),
credentials: 'omit'
});
if (response.status === 429) {
const seconds = Math.max(1, Number(response.headers.get('Retry-After')) || 1);
retryAt = Date.now() + seconds * 1000;
throw new Error(
`Request limit or credit balance reached. Retry in ${seconds} seconds; check your dashboard if it persists.`
);
}
if (response.status === 404)
throw new Error('This suggestion expired. Type again to get a fresh list.');
if (response.status === 401)
throw new Error('Check that your API key is active and copied correctly.');
if (response.status === 403)
throw new Error(`Add ${location.origin} to the browser key's allowed origins.`);
if (!response.ok)
throw new Error(`The request failed (HTTP ${response.status}). Try again later.`);
return response.json();
}
function failure(error) {
status.textContent =
error.name === 'TypeError'
? 'Could not reach the API. Check your connection and the browser key allowed origins.'
: error.name === 'TimeoutError'
? 'The request timed out. Type again to retry.'
: error.message;
}
async function select(suggestion) {
const mine = cancel();
close();
input.value = suggestion.displayName;
retrieving = true;
status.textContent = 'Retrieving the selected location…';
output.textContent = '';
try {
const place = await call('retrieve', { id: suggestion.id });
if (mine !== sequence) return;
if (!place.geoCoordinates)
throw new Error('No coordinates returned for this suggestion.');
output.textContent = `${suggestion.displayName}\nLatitude: ${place.geoCoordinates.lat ?? 0}\nLongitude: ${place.geoCoordinates.lon ?? 0}`;
status.textContent = 'Location selected.';
} catch (error) {
if (mine === sequence) failure(error);
} finally {
if (mine === sequence) retrieving = false;
}
}
input.addEventListener('input', (event) => {
const mine = cancel();
close();
output.textContent = '';
const text = input.value.trim();
if (event.isComposing || text.length < 2) {
status.textContent = 'Type at least two characters.';
return;
}
status.textContent = 'Waiting for typing to pause…';
timer = setTimeout(async () => {
status.textContent = 'Loading suggestions…';
try {
const body = await call('autocomplete', { text, limit: 5 });
if (mine !== sequence) return;
suggestions = body.suggestions ?? [];
for (const [index, suggestion] of suggestions.entries()) {
const item = document.createElement('li');
item.setAttribute('role', 'none');
const button = document.createElement('button');
button.type = 'button';
button.id = `suggestion-${index}`;
button.tabIndex = -1;
button.setAttribute('role', 'option');
button.setAttribute('aria-selected', 'false');
button.textContent = suggestion.displayName;
button.addEventListener('pointerdown', (event) => event.preventDefault());
button.addEventListener('click', () => select(suggestion));
item.append(button);
list.append(item);
}
list.hidden = suggestions.length === 0;
input.setAttribute('aria-expanded', String(!list.hidden));
status.textContent = suggestions.length
? `${suggestions.length} suggestions. Use the arrow keys and Enter to select.`
: 'No suggestions. Try a longer address, postcode or nearby city.';
} catch (error) {
if (mine === sequence) failure(error);
}
}, 500);
});
input.addEventListener('keydown', (event) => {
if (event.isComposing) return;
if (event.key === 'Escape') {
cancel();
close();
status.textContent = 'Suggestions dismissed.';
} else if (suggestions.length && ['ArrowDown', 'ArrowUp'].includes(event.key)) {
event.preventDefault();
active =
event.key === 'ArrowDown'
? (active + 1) % suggestions.length
: active <= 0
? suggestions.length - 1
: active - 1;
for (const [index, option] of [...list.querySelectorAll('[role="option"]')].entries()) {
option.setAttribute('aria-selected', String(index === active));
}
input.setAttribute('aria-activedescendant', `suggestion-${active}`);
} else if (event.key === 'Enter' && active >= 0) {
event.preventDefault();
select(suggestions[active]);
}
});
input.addEventListener('blur', () => {
if (!retrieving) {
cancel();
status.textContent = 'Suggestions dismissed.';
}
close();
});
</script>
</body>
</html>
Leaflet address autocomplete
Add search to a Leaflet map.
Leaflet GeoSearch includes an OpenGeocoding provider. The control displays suggestions while
typing and retrieves the selected ID before moving the map. This example limits suggestions
to Denmark; change or remove countryCode for your application.
In an existing JavaScript app with a bundler such as Vite, install these versions, add the
map element to your HTML, and use the JavaScript below. Replace apiKey with a browser key allowing your app's exact origin.
npm install [email protected] [email protected] <div id="map" style="height: 400px"></div> import L from 'leaflet';
import { GeoSearchControl, OpenGeocodingProvider } from 'leaflet-geosearch';
import 'leaflet/dist/leaflet.css';
import 'leaflet-geosearch/dist/geosearch.css';
const map = L.map('map').setView([55.6761, 12.5683], 12);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
map.attributionControl.addAttribution(
'<a href="https://opengeocoding.com/legal/attributions">Geocoding data credits</a>'
);
const provider = new OpenGeocodingProvider({
apiKey: 'og_YOUR_BROWSER_KEY',
params: { countryCode: 'dk', limit: 5 }
});
map.addControl(
new GeoSearchControl({
provider,
style: 'bar',
autoComplete: true,
autoCompleteDelay: 500,
keepResult: true,
showMarker: false
})
);
let marker;
map.on('geosearch/showlocation', ({ location }) => {
marker?.remove();
marker = L.circleMarker([location.y, location.x], { radius: 8 }).addTo(map);
});
Fewer typos in, cleaner addresses out.
Suggestions spend no credits
Every keystroke is free. You spend one credit only when a user picks a suggestion and you retrieve its coordinates.
Countries down to house numbers
Suggestions cover countries, regions, cities, streets, postcodes and addresses, with an optional set of notable landmarks.
Filter and bias
Restrict to one country, or pass the user's position so nearby places rank higher.
Unit text kept, not invented
Floor or apartment text the user typed stays in the label and is flagged as unverified, while the ID still points at the building.
Address autocomplete without Google or the public Nominatim API.
The public Nominatim server runs on donated capacity to power the search on openstreetmap.org. Its usage policy forbids client-side auto-complete search on its API and allows at most 1 request per second for your whole app, so a search box that queries it on every keystroke can get banned. These are the usual alternatives.
Photon's demo server
Built for search as you type, on OpenStreetMap data. The public server is free within reasonable use, with no availability guarantee; heavy use is throttled or banned.
Self-hosted Photon or Nominatim
No usage policy but your own. You run the servers, the imports and the updates. A full Nominatim planet needs about 128 GB of RAM.
OpenGeocoding
Hosted, on OpenStreetMap and other open data. Suggestions spend no credits, and your limit is your plan's per-second rate, not a shared server's capacity.
Nominatim policy checked , Photon terms checked . OpenGeocoding compared with Nominatim →
Size for your peak typing traffic.
Autocomplete is authenticated and shares your account's per-second limit with every other call. Debounce keystrokes, and call it from the page with a browser key restricted to your site, or proxy requests through your backend with a secret key. The free plan allows 2 requests per second; Starter allows 5 and Growth allows 25. Enterprise capacity is sized to your traffic.
Handle empty results and errors.
No suggestions
An empty or omitted suggestions list is a valid response. Ask for a longer
address or postcode, check any country filter, and allow manual entry rather than inventing
an address. Check the coverage evidence and its limitations →
Authentication or browser-origin errors
For HTTP 401, check that the key is active. For HTTP 403 or a browser CORS error, check that it is a browser key and that its allowed origins include the exact scheme, hostname and port. Never fix this by exposing a secret key.
HTTP 429: pause, do not loop
Respect Retry-After. All account keys and operations share the rate limit; a
debounce alone cannot guarantee capacity. Retrieval also needs credits. Autocomplete remains
credit-free at zero balance. The browser example waits for another user action instead of
retrying automatically.
HTTP 404: refresh the suggestions
IDs belong to an index snapshot. If a selected ID expires, request new suggestions and let the user choose again. Ignore late replies after the input changes. A completed retrieval can still spend a credit even if the browser cancels or ignores its reply.
Add autocomplete to your form.
The same key covers forward geocoding, reverse geocoding and bulk jobs.
Get a free API key