Address autocomplete in plain JavaScript
About 10 minutes. Updated 11 September 2026.
Add Australian address autocomplete to any HTML form with one script and no framework: a debounced call to the address API as someone types, a list to pick from, and the G-NAF id saved with the form.
This is the smallest useful integration: an input, a list under it, and a hidden field that carries the G-NAF id into whatever handles the form. It works in any page, with any backend, and there is nothing to install.
01The markup
A search input, an empty list for suggestions, and a hidden input for the id. The hidden field is the point of the exercise: store the id against your record and you can read the address back a year later without knowing how it was typed.
<label for="address">Delivery address</label>
<input id="address" type="search" autocomplete="off" placeholder="145 sydney road">
<ul id="suggestions" hidden></ul>
<input type="hidden" name="gnaf_pid" id="gnaf_pid">
<script type="module" src="/address-search.js"></script>02The script
Wait for 200ms of quiet, then call /v1/addresses with what has been typed so far. Every result is already a resolved address, so picking one needs no second call. An in flight request is abandoned when the next one starts, so a slow answer for "14" cannot land after the fast one for "145 sy".
// address-search.js
const KEY = "lc_live_..."; // a public key, with this site under its allowed sites
const API = "https://api.locio.com.au";
const input = document.querySelector("#address");
const list = document.querySelector("#suggestions");
const hidden = document.querySelector("#gnaf_pid");
let timer;
let inflight;
input.addEventListener("input", () => {
clearTimeout(timer);
hidden.value = "";
const q = input.value.trim();
if (q.length < 3) {
list.hidden = true;
return;
}
// 200ms of quiet before asking. Every request is a unit, and one per
// keystroke buys nothing a slightly later one would not.
timer = setTimeout(() => search(q), 200);
});
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; // 402 is quota, 429 is rate; both are worth logging
const { data } = await res.json();
render(data);
} catch (err) {
if (err.name !== "AbortError") console.error(err);
}
}
function render(addresses) {
list.replaceChildren(
...addresses.map((address) => {
const item = document.createElement("li");
const button = document.createElement("button");
button.type = "button";
button.textContent = address.formatted;
button.addEventListener("click", () => pick(address));
item.append(button);
return item;
}),
);
list.hidden = addresses.length === 0;
}
function pick(address) {
input.value = address.formatted;
hidden.value = address.gnaf_pid; // store this, not the text
list.hidden = true;
input.dispatchEvent(new CustomEvent("address:picked", { detail: address, bubbles: true }));
}03Use what was picked
The script fires an event with the whole address on it. Coordinates, the ABS mesh block and every G-NAF component are there without another request.
document.addEventListener("address:picked", (event) => {
const { gnaf_pid, lat, lng, components } = event.detail;
console.log(gnaf_pid, lat, lng, components.postcode);
});What it costs
One unit per request, and the debounce keeps that to a handful per address. The free tier covers 10,000 a month, which is a few thousand completed addresses. Refused requests are free.
Related
- Address autocomplete in React
- Address autocomplete in Vue
- Stand up a React site with Australian address search
- Put a searched address on a map with MapLibre
- 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.