When using Google’s Reverse Geocoding API, selecting a consistent “neighborhood” or “area” label across different countries is not straightforward.
["political", "neighborhood"]
["administrative_area_level_2", "political"]
Other countries may use sublocality_level_1, locality, or skip levels entirely.
["administrative_area_level_5", "political"]
That works globally.
-
Works across countries
-
Handles missing administrative levels
-
Falls back gracefully
-
Returns either:
- A street-level address
- Or the best available “neighborhood-like” label
Instead of hardcoding one types combination, we:
1️⃣ Rank result types by priority
const STREET_RESULT_TYPE_PRIORITY = [
'neighborhood',
'street_address',
'administrative_area_level_2',
'administrative_area_level_1',
'premise',
'subpremise',
'route',
];
2️⃣ Rank address components inside results
const AREA_COMPONENT_TYPE_PRIORITY = [
'neighborhood',
'sublocality_level_1',
'sublocality_level_2',
'sublocality_level_3',
'sublocality',
'locality',
'administrative_area_level_3',
'administrative_area_level_2',
'administrative_area_level_1',
'country',
];
This creates a fallback ladder that adapts to different country structures.
GOOGLE_MAPS_API_KEY="YOUR_KEY" node reverse-geocode-test.js -1.286389 36.817223
GOOGLE_MAPS_API_KEY="YOUR_KEY" node reverse-geocode-test.js 37.4219999 -122.0840575
/**
* reverse-geocode-test.js
*
* Usage:
* GOOGLE_MAPS_API_KEY="YOUR_KEY" node reverse-geocode-test.js -1.286389 36.817223
* GOOGLE_MAPS_API_KEY="YOUR_KEY" node reverse-geocode-test.js 37.4219999 -122.0840575
*
* Notes:
* - Uses reverse geocoding (latlng)
* - Tries to generalize across countries using a ranking strategy
*/
const API_KEY = process.env.GOOGLE_MAPS_API_KEY;
if (!API_KEY) {
console.error('Missing GOOGLE_MAPS_API_KEY env var.');
process.exit(1);
}
const args = process.argv.slice(2);
if (args.length < 2) {
console.error(
'Provide LAT and LNG.\nExample: node reverse-geocode-test.js -1.286389 36.817223',
);
process.exit(1);
}
const lat = Number(args[0]);
const lng = Number(args[1]);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
console.error('LAT/LNG must be numbers.');
process.exit(1);
}
// --- ranking logic ---
// Street-level “best” preference (more specific first)
const STREET_RESULT_TYPE_PRIORITY = [
'neighborhood',
'street_address',
'administrative_area_level_2',
'administrative_area_level_1',
'premise',
'subpremise',
'route', // may not include street_number; still often useful
];
// Neighborhood-ish preference inside address_components (more specific first)
const AREA_COMPONENT_TYPE_PRIORITY = [
'neighborhood',
'sublocality_level_1',
'sublocality_level_2',
'sublocality_level_3',
'sublocality',
'locality',
'administrative_area_level_3',
'administrative_area_level_2',
'administrative_area_level_1',
'country',
];
function hasAnyType(objTypes = [], wantedTypes = []) {
return wantedTypes.some((t) => objTypes.includes(t));
}
function pickBestStreetResult(results) {
for (const wanted of STREET_RESULT_TYPE_PRIORITY) {
const hit = results.find((r) => (r.types || []).includes(wanted));
if (hit) return hit;
}
// fallback: first result (Google already sorts by relevance)
return results[0] || null;
}
function getComponent(components, type) {
return (components || []).find((c) => (c.types || []).includes(type)) || null;
}
function pickBestAreaComponent(result) {
const components = result?.address_components || [];
console.log('All components:', components);
for (const t of AREA_COMPONENT_TYPE_PRIORITY) {
const c = getComponent(components, t);
if (c) {
console.log(c);
return { type: t, value: c.long_name };
}
}
return null;
}
function composeAreaLabel(result) {
const comps = result?.address_components || [];
const pick = (t) => getComponent(comps, t)?.long_name;
const neighborhoodLike =
pick('neighborhood') ||
pick('sublocality_level_1') ||
pick('sublocality_level_2') ||
pick('sublocality_level_3') ||
pick('sublocality');
const locality = pick('locality');
const admin1 = pick('administrative_area_level_1');
const country = pick('country');
// Build a clean label without duplicates / empties
const parts = [neighborhoodLike, locality, admin1, country].filter(Boolean);
// De-dupe consecutive duplicates
const deduped = [];
for (const p of parts) {
if (deduped[deduped.length - 1] !== p) deduped.push(p);
}
return deduped.join(', ');
}
// Optional: ask Google to bias towards address-y results
// NOTE: Google supports `result_type` and `location_type` parameters in reverse requests.
const RESULT_TYPE =
'street_address|premise|subpremise|route|neighborhood|sublocality|locality|administrative_area_level_2|administrative_area_level_1';
const LOCATION_TYPE = 'ROOFTOP|RANGE_INTERPOLATED|GEOMETRIC_CENTER|APPROXIMATE'; // include all, still lets you inspect
async function reverseGeocode(lat, lng) {
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json');
url.searchParams.set('latlng', `${lat},${lng}`);
url.searchParams.set('key', API_KEY);
url.searchParams.set('result_type', RESULT_TYPE);
url.searchParams.set('location_type', LOCATION_TYPE);
const res = await fetch(url.toString());
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
return res.json();
}
function summarizeResults(results) {
return (results || []).slice(0, 8).map((r, idx) => ({
i: idx,
formatted_address: r.formatted_address,
types: r.types,
location_type: r.geometry?.location_type,
}));
}
(async () => {
try {
const data = await reverseGeocode(lat, lng);
if (data.status !== 'OK') {
console.error('API status:', data.status);
console.error('Error message:', data.error_message || '(none)');
process.exit(1);
}
const results = data.results || [];
const bestStreet = pickBestStreetResult(results);
const bestAreaComp = bestStreet ? pickBestAreaComponent(bestStreet) : null;
const areaLabel = bestStreet ? composeAreaLabel(bestStreet) : '';
console.log('\n=== INPUT ===');
console.log({ lat, lng });
console.log('\n=== BEST STREET-LEVEL RESULT ===');
console.log(bestStreet ? bestStreet.formatted_address : '(none)');
console.log('types:', bestStreet?.types || []);
console.log('location_type:', bestStreet?.geometry?.location_type);
console.log('\n=== AREA / NEIGHBORHOOD-LIKE (FALLBACK LADDER) ===');
console.log(areaLabel || '(none)');
if (bestAreaComp) console.log('top component:', bestAreaComp);
console.log('\n=== TOP RESULTS (for debugging) ===');
console.table(summarizeResults(results));
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
})();✅ Country-to-country inconsistencies
Administrative levels are not standardized globally.
✅ Missing data
Some locations don’t return neighborhood. Others don’t return locality.
✅ Overuse of political
political appears almost everywhere and cannot be used alone for filtering.
✅ Clean fallback logic
The script always finds the most specific meaningful label available.
Westlands, Nairobi, Kenya
types: [ 'neighborhood', 'political' ]
=== AREA / NEIGHBORHOOD-LIKE (FALLBACK LADDER) ===
Westlands, Nairobi, Nairobi County, Kenya
Instead of assuming:
“Every country has administrative_area_level_5”
We assume:
“Every country has some smallest meaningful named area — find it.”
That mental shift is what makes this robust.
If you’re building:
-
Delivery apps
-
Ride-hailing systems
-
Geofencing services
-
Property platforms
-
Location-based marketplaces
You cannot rely on one static address type.
This approach makes reverse geocoding production-safe across regions.
-
Cache results to reduce API costs
-
Add precision filtering (ROOFTOP vs APPROXIMATE)
-
Store both:
- Precise address
- Stable area label
-
Convert to a reusable npm module
Built to solve real-world cross-country reverse geocoding inconsistencies.
If this helped you, feel free to share or adapt it.