DEV Community

ApogeoAPI
ApogeoAPI

Posted on • Originally published at apogeoapi.com

Building a City Autocomplete with 150K Cities — Tutorial

City autocomplete sounds simple until you realize you need to search across 150,000 cities from 250 countries — with fast response times and no UI jank. Here's how to do it right.

The Challenge

You can't load 150K cities into the browser — that's tens of megabytes. You need server-side search that returns relevant results as the user types. ApogeoAPI's global search endpoint handles this.

The API Approach

Use the global search endpoint for cross-country city search:

GET /v1/search?q=lon&limit=5
// Returns cities, states, and countries matching "lon"
// Results: London GB, Long Beach US, Longueuil CA, ...
Enter fullscreen mode Exit fullscreen mode

Step 1: Build the Search Hook with Debounce

// hooks/useCitySearch.ts
import { useState, useEffect } from 'react';
function useDebounce(value: T, ms: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(timer);
}, [value, ms]);
return debounced;
}
interface CityResult {
name: string;
stateCode: string;
stateName: string;
countryCode: string;
countryName: string;
latitude: number;
longitude: number;
}
export function useCitySearch(query: string) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery.length  r.json())
.then(data => setResults(data.cities ?? []))
.finally(() => setLoading(false));
}, [debouncedQuery]);
return { results, loading };
}
Enter fullscreen mode Exit fullscreen mode

Step 2: The Autocomplete Component

'use client';
import { useState } from 'react';
import { useCitySearch } from '@/hooks/useCitySearch';
interface Props {
onSelect: (city: { name: string; country: string; lat: number; lon: number }) => void;
}
export function CityAutocomplete({ onSelect }: Props) {
const [query, setQuery] = useState('');
const { results, loading } = useCitySearch(query);
return (

 setQuery(e.target.value)}
placeholder="Search city..."
className="w-full border rounded px-3 py-2"
/>
{loading && (
...
)}
{results.length > 0 && (

{results.map(city => (
 {
onSelect({ name: city.name, country: city.countryCode,
lat: city.latitude, lon: city.longitude });
setQuery(`${city.name}, ${city.stateName}, ${city.countryCode}`);
// close dropdown implicitly by clearing results
}}
className="px-4 py-2.5 hover:bg-gray-50 cursor-pointer"
>
{city.name}

{city.stateName}, {city.countryCode}

))}

)}

);
}
Enter fullscreen mode Exit fullscreen mode

Performance Notes

  • Debounce 300ms: Prevents a request on every keystroke.
  • Minimum 2 characters: Avoids overly broad results on single letters.
  • Limit to 8 results: Users don't scroll through more than that in a dropdown.
  • Cache recent searches: Store the last 10 queries in a useRef Map to avoid re-fetching the same query.

Originally published at https://apogeoapi.com/blog/city-autocomplete-api. Try ApogeoAPI free at apogeoapi.com.

Top comments (0)