Skip to content

Instantly share code, notes, and snippets.

@ammarhaiderak
Created February 24, 2026 22:09
Show Gist options
  • Select an option

  • Save ammarhaiderak/1b8a917edf5bee629d1645ff178c30cc to your computer and use it in GitHub Desktop.

Select an option

Save ammarhaiderak/1b8a917edf5bee629d1645ff178c30cc to your computer and use it in GitHub Desktop.

🌍 Robust Reverse Geocoding Across Countries (Google Maps API)

When using Google’s Reverse Geocoding API, selecting a consistent “neighborhood” or “area” label across different countries is not straightforward.

Different countries structure address hierarchies differently:

🇰🇪 Kenya might return:

["political", "neighborhood"]

🇺🇸 USA might return:

["administrative_area_level_2", "political"]

Other countries may use sublocality_level_1, locality, or skip levels entirely.

There is no universal rule like:

["administrative_area_level_5", "political"]

That works globally.

🎯 The Goal

Create a generalized ranking strategy that:

  • Works across countries

  • Handles missing administrative levels

  • Falls back gracefully

  • Returns either:

    • A street-level address
    • Or the best available “neighborhood-like” label

🧠 The Strategy

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.

🚀 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

📦 Full Script

/**
 * 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);
  }
})();

🔍 What This Solves

✅ 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.

🧪 Example Output

Westlands, Nairobi, Kenya
types: [ 'neighborhood', 'political' ]

=== AREA / NEIGHBORHOOD-LIKE (FALLBACK LADDER) ===
Westlands, Nairobi, Nairobi County, Kenya

🏗 Architecture Insight

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.

📌 Why This Matters

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.

💡 Future Improvements

  • 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

🧑‍💻 Author

Built to solve real-world cross-country reverse geocoding inconsistencies.

If this helped you, feel free to share or adapt it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment