City Search
Search cities by name to retrieve timezones, coordinates, and populations for building city autocomplete inputs.
This endpoint only searches the city database. It does not run Swiss Ephemeris and does not count against your Chart requests or SAGE responses. Location tools are free with any key.
Overview
Chart endpoints require a valid birthLocation. This endpoint queries the offline city database used by the THD frontend, containing 167,000+ cities across 246 countries.
Typical flow:
- User types into a city input field
- Call
/api/locations?query=<input>on each keystroke (debounced) - Display the returned list as a dropdown
- Pass the chosen
valueasbirthLocationin the chart request
Request
Method: GET
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | — | Partial city name, min 3 characters |
limit | number | No | 8 | Max results returned (max 20) |
Response
Returns a JSON array sorted by population descending (most prominent city wins ties).
| Field | Description |
|---|---|
name | City name |
province | State or province (null if not applicable) |
country | Full country name |
timezone | IANA timezone identifier |
coordinates | { lat, lng } object. Pass these as latitude/longitude in chart requests to bypass city lookup. |
population | Population used for ranking (higher ranks first). |
value | Display string for UIs. Pass this as birthLocation in chart requests. |
Error: query too short
{ "success": false, "error": "query param is required and must be at least 3 characters." }
Dataset
- Source: GeoNames
cities1000(CC-BY 4.0) - Coverage: 167,617 cities, population ≥ 1,000, 246 countries
- US cities: 17,321 (covers towns down to ~1,000 population)
- Search: prefix-first then contains, diacritics-insensitive (
"Zurich"matches"Zürich") - Tiebreaker: population descending (
"London"returns UK (8.9M) before Canada (422K)) - Comma hint:
"London, Canada"filters by province/country
Towns with fewer than 1,000 residents are not in the dataset. For very remote birthplaces, collect coordinates directly from the user and pass latitude and longitude to the chart endpoint to bypass the city lookup entirely.
Integration example
// Debounced autocomplete
let debounce;
cityInput.addEventListener('input', (e) => {
clearTimeout(debounce);
if (e.target.value.length < 3) return;
debounce = setTimeout(async () => {
const res = await fetch(
`/api/locations?query=${encodeURIComponent(e.target.value)}&limit=8`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const cities = await res.json();
// Render dropdown
dropdown.innerHTML = cities.map(city =>
`<option value="${city.value}" data-lat="${city.coordinates.lat}" data-lng="${city.coordinates.lng}">
${city.value}
</option>`
).join('');
}, 250);
});