DEV Community

Cleyson Mayrink
Cleyson Mayrink

Posted on

How to build a fast global city autocomplete in JavaScript

If you have ever built an address form, shipping calculator, or travel app, you know the struggle: you need a reliable way for users to search for their city or state, but you do not want to maintain a multi-gigabyte geospatial database just for an autocomplete dropdown.

Hosting a dedicated GIS database with hundreds of thousands of cities and coordinates can be overkill for most web projects. On the other hand, many public location services have strict rate limits, high latency on keystrokes, or lack fine-grained radius search.

To solve this for my own projects, I built GeoPulse: an API designed specifically for fast city search, radius lookups, and administrative hierarchies with over 800,000 locations worldwide.

In this guide, I will show you how to implement a clean city autocomplete input and how to run radius-based distance queries.


The Data Under the Hood

The dataset behind GeoPulse merges official GeoNames records with Wikidata entities:

  • 800,000+ populated places: Global capitals, cities, towns, and municipalities.
  • Hierarchical divisions: Country -> Region/State -> Sub-division -> City.
  • Coordinates & Timezones: Latitude, longitude, elevation, and real-time local timestamps adjusted for daylight saving time.

1. Building a Fast City Autocomplete

When a user types into an input field, you want to query cities matching the prefix, but only after they stop typing for 250ms (debounce) to avoid wasting requests.

Here is a clean vanilla JavaScript implementation:

<input type="text" id="cityInput" placeholder="Start typing a city (e.g. San, Tok, Lond)..." />
<ul id="results"></ul>
Enter fullscreen mode Exit fullscreen mode
const input = document.getElementById('cityInput');
const list = document.getElementById('results');
let debounceTimer;

input.addEventListener('input', (e) => {
  clearTimeout(debounceTimer);
  const query = e.target.value.trim();

  if (query.length < 2) {
    list.innerHTML = '';
    return;
  }

  debounceTimer = setTimeout(() => {
    fetchCities(query);
  }, 250);
});

async function fetchCities(prefix) {
  const url = `https://geopulse-global-cities-geolocation-api1.p.rapidapi.com/geo/cities?namePrefix=${encodeURIComponent(prefix)}&limit=5&sort=-population`;

  try {
    const res = await fetch(url, {
      headers: {
        'x-rapidapi-key': 'YOUR_RAPIDAPI_KEY',
        'x-rapidapi-host': 'geopulse-global-cities-geolocation-api1.p.rapidapi.com'
      }
    });

    const body = await res.json();
    renderResults(body.data || []);
  } catch (err) {
    console.error(err);
  }
}

function renderResults(cities) {
  list.innerHTML = cities
    .map(c => `<li><strong>${c.name}</strong>, ${c.region} (${c.countryCode}) - Pop: ${c.population.toLocaleString()}</li>`)
    .join('');
}
Enter fullscreen mode Exit fullscreen mode

API Response Format

Here is the exact JSON structure returned by the endpoint:

{
  "metadata": {
    "currentOffset": 0,
    "totalCount": 1842
  },
  "data": [
    {
      "id": 3448439,
      "wikiDataId": "Q174",
      "type": "CITY",
      "name": "Sao Paulo",
      "country": "Brazil",
      "countryCode": "BR",
      "region": "Sao Paulo",
      "regionCode": "SP",
      "latitude": -23.5475,
      "longitude": -46.63611,
      "population": 12400232,
      "timezone": "America/Sao_Paulo"
    },
    {
      "id": 3871336,
      "wikiDataId": "Q2887",
      "type": "CITY",
      "name": "Santiago",
      "country": "Chile",
      "countryCode": "CL",
      "region": "Region Metropolitana",
      "regionCode": "RM",
      "latitude": -33.45694,
      "longitude": -70.64827,
      "population": 5614000,
      "timezone": "America/Santiago"
    },
    {
      "id": 4726206,
      "wikiDataId": "Q16552",
      "type": "CITY",
      "name": "San Antonio",
      "country": "United States",
      "countryCode": "US",
      "region": "Texas",
      "regionCode": "TX",
      "latitude": 29.42412,
      "longitude": -98.49363,
      "population": 1434625,
      "timezone": "America/Chicago"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Sorting by -population ensures that the most prominent matching cities (like Sao Paulo, Santiago, or San Antonio) show up first.


2. Finding Nearby Cities Within a Radius

Another common problem is: "Show me all cities within 50 miles of a target city."

Instead of downloading math libraries to calculate haversine formulas across 800k records on your server, you can query it in a single API call:

async function getNearbyCities(cityId, radiusMiles = 50) {
  const url = `https://geopulse-global-cities-geolocation-api1.p.rapidapi.com/geo/cities/${cityId}/nearbyCities?radius=${radiusMiles}&distanceUnit=MI&limit=3`;

  const res = await fetch(url, {
    headers: {
      'x-rapidapi-key': 'YOUR_RAPIDAPI_KEY',
      'x-rapidapi-host': 'geopulse-global-cities-geolocation-api1.p.rapidapi.com'
    }
  });

  const data = await res.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

API Response Format

{
  "metadata": {
    "currentOffset": 0,
    "totalCount": 87
  },
  "data": [
    {
      "id": 3461786,
      "wikiDataId": "Q175",
      "type": "CITY",
      "name": "Guarulhos",
      "country": "Brazil",
      "countryCode": "BR",
      "region": "Sao Paulo",
      "distance": 8.74,
      "latitude": -23.46278,
      "longitude": -46.53333,
      "population": 1345364
    },
    {
      "id": 3452925,
      "wikiDataId": "Q175",
      "type": "CITY",
      "name": "Osasco",
      "country": "Brazil",
      "countryCode": "BR",
      "region": "Sao Paulo",
      "distance": 9.53,
      "latitude": -23.5325,
      "longitude": -46.79167,
      "population": 699944
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each record returns the exact distance relative to the center city in the requested unit (MI or KM).


3. Calculating Distance Between Two Cities

If you need straight-line geodesic distance between two points (useful for delivery estimates or proximity scoring):

async function getDistance(fromCityId, toCityId) {
  const url = `https://geopulse-global-cities-geolocation-api1.p.rapidapi.com/geo/cities/${fromCityId}/distance?toCityId=${toCityId}&distanceUnit=KM`;

  const res = await fetch(url, {
    headers: {
      'x-rapidapi-key': 'YOUR_RAPIDAPI_KEY',
      'x-rapidapi-host': 'geopulse-global-cities-geolocation-api1.p.rapidapi.com'
    }
  });

  const result = await res.json();
  return result;
}
Enter fullscreen mode Exit fullscreen mode

API Response Format

{
  "data": 357.24
}
Enter fullscreen mode Exit fullscreen mode

API Reference & Playground

All 39 endpoints (including country profiles, timezones, currency mappings, and administrative divisions) are documented with live test capabilities:

There is a free tier available on RapidAPI with enough quota to test and build small projects.

If you test it or have ideas for additional fields or filters that would make your workflow easier, drop a comment below!

Top comments (0)