A self-contained autocomplete component. It debounces typing, cancels stale requests with AbortController, and calls onSelect with the chosen GeoJSON feature. Use a publishable key whose allowed origins include your site (and http://localhost:5173 on a development key).
The component
// AddressAutocomplete.tsx
import { useEffect, useId, useRef, useState } from 'react';
export interface Place {
type: 'Feature';
geometry: { type: 'Point'; coordinates: [number, number] };
properties: { gid: string; label: string; postalcode?: string; locality?: string; region?: string; country_code?: string };
}
const KEY = import.meta.env.VITE_GEOLITH_KEY as string; // gc_pk_…
export function AddressAutocomplete({ onSelect }: { onSelect: (place: Place) => void }) {
const [text, setText] = useState('');
const [places, setPlaces] = useState<Place[]>([]);
const listId = useId();
const picked = useRef('');
useEffect(() => {
if (text.trim().length < 3 || text === picked.current) {
setPlaces([]);
return;
}
const controller = new AbortController();
const timer = setTimeout(async () => {
const url = new URL('https://api.geolith.app/v1/autocomplete');
url.searchParams.set('text', text);
url.searchParams.set('size', '5');
url.searchParams.set('api_key', KEY);
try {
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) return;
const body = (await res.json()) as { features: Place[] };
setPlaces(body.features);
} catch (error) {
if ((error as Error).name !== 'AbortError') throw error;
}
}, 200);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [text]);
return (
<div>
<input
type="search"
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="Start typing an address"
aria-controls={listId}
autoComplete="off"
/>
<ul id={listId} role="listbox">
{places.map((place) => (
<li key={place.properties.gid} role="option" aria-selected={false}>
<button
type="button"
onClick={() => {
picked.current = place.properties.label;
setText(place.properties.label);
setPlaces([]);
onSelect(place);
}}
>
{place.properties.label}
</button>
</li>
))}
</ul>
<small>© OpenStreetMap contributors (ODbL), Overture Maps Foundation</small>
</div>
);
}
Use it
// App.tsx
import { AddressAutocomplete, type Place } from './AddressAutocomplete';
export default function App() {
return (
<AddressAutocomplete
onSelect={(place: Place) => {
const [lon, lat] = place.geometry.coordinates;
console.log(place.properties.label, place.properties.postalcode, lat, lon);
}}
/>
);
}
Put the key in .env.local as VITE_GEOLITH_KEY=gc_pk_… (Vite). In Next.js, use NEXT_PUBLIC_GEOLITH_KEY and process.env.NEXT_PUBLIC_GEOLITH_KEY, and add 'use client' at the top of the component file.