Address autocomplete in Vue
About 15 minutes. Updated 11 September 2026.
A single file Vue 3 component that adds Australian address autocomplete to a form with the Locio API: script setup, a watched query with a debounce, request cancellation, and the picked address emitted to the parent.
One single file component. The query is a ref, a watcher debounces it and makes the request, and the picked address is emitted upward as a typed object so the parent can store the G-NAF id and use the coordinates.
01The component
The watcher does the work that usually goes wrong: it clears the previous timer and aborts the previous request on every change, so a slow answer for an earlier prefix cannot arrive after a newer one. Results come from /v1/addresses, and each is a resolved address.
<!-- AddressSearch.vue -->
<script setup lang="ts">
import { ref, watch } from "vue";
type Address = {
gnaf_pid: string;
formatted: string;
lat: number;
lng: number;
mesh_block?: string;
components: Record<string, string>;
};
const KEY = import.meta.env.VITE_LOCIO_KEY as string; // a public key
const API = "https://api.locio.com.au";
const emit = defineEmits<{ pick: [address: Address] }>();
const query = ref("");
const results = ref<Address[]>([]);
const picked = ref<Address | null>(null);
const busy = ref(false);
let timer: ReturnType<typeof setTimeout> | undefined;
let inflight: AbortController | undefined;
watch(query, (value) => {
clearTimeout(timer);
inflight?.abort();
picked.value = null;
const q = value.trim();
if (q.length < 3) {
results.value = [];
return;
}
// 200ms of quiet before asking: one request per pause, not per keystroke.
timer = setTimeout(() => search(q), 200);
});
async function search(q: string) {
inflight = new AbortController();
busy.value = true;
try {
const url = new URL(`${API}/v1/addresses`);
url.searchParams.set("q", q);
url.searchParams.set("limit", "8");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
signal: inflight.signal,
});
if (res.ok) results.value = ((await res.json()) as { data: Address[] }).data;
} catch (err) {
if ((err as Error).name !== "AbortError") console.error(err);
} finally {
busy.value = false;
}
}
function pick(address: Address) {
picked.value = address;
query.value = address.formatted;
results.value = [];
emit("pick", address);
}
</script>
<template>
<div>
<label for="address">Delivery address</label>
<input id="address" v-model="query" type="search" autocomplete="off" placeholder="145 sydney road" />
<p v-if="busy">Searching…</p>
<ul v-if="results.length && !picked" role="listbox">
<li v-for="address in results" :key="address.gnaf_pid">
<button type="button" @click="pick(address)">{{ address.formatted }}</button>
</li>
</ul>
<!-- The id is what your form should submit, not the text. -->
<input type="hidden" name="gnaf_pid" :value="picked?.gnaf_pid ?? ''" />
</div>
</template>02Use it
<script setup lang="ts">
import AddressSearch from "./AddressSearch.vue";
</script>
<template>
<form method="post" action="/checkout">
<AddressSearch @pick="(address) => console.log(address.gnaf_pid, address.lat, address.lng)" />
<button type="submit">Continue</button>
</form>
</template>What it costs
One unit per request. With the debounce, a completed address is usually three or four requests, so the free 10,000 a month covers a few thousand addresses. Refused requests are free.
Related
- Address autocomplete in plain JavaScript
- Address autocomplete in React
- 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.