DEV Community

Cover image for Location APIs: Distance, Geocoding, and Timezones
APIVerve
APIVerve

Posted on • Originally published at blog.apiverve.com

Location APIs: Distance, Geocoding, and Timezones

A delivery app I consulted for was using Google Maps APIs for everything. Distance calculations, address validation, timezone lookups—all Google.

Their monthly bill: $4,200.

For a startup doing 50,000 deliveries a month, that's $0.08 per delivery just for location math. The actual delivery cost was $5. Their location APIs were 1.6% of gross revenue, doing nothing but basic math.

We replaced most of it with simpler APIs. Same functionality. Monthly cost: $180.

You don't need Google Maps for distance calculations. You don't need Google for geocoding basic addresses. And you definitely don't need Google to look up what timezone a city is in.

Distance: The Haversine Formula

The distance between two points on Earth is just math. The Haversine formula has been around since the 1800s. There's nothing proprietary about it.

async function getDistance(from, to) {
  const res = await fetch(
    `https://api.apiverve.com/v1/distancecalculator?lat1=${from.lat}&lon1=${from.lon}&lat2=${to.lat}&lon2=${to.lon}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    kilometers: data.kilometers,
    miles: data.miles,
    nauticalMiles: data.nauticalMiles
  };
}

// Usage
const distance = await getDistance(
  { lat: 40.7128, lon: -74.0060 },  // New York
  { lat: 34.0522, lon: -118.2437 }  // Los Angeles
);
// { kilometers: 3935.75, miles: 2445.56, nauticalMiles: 2125.27 }
Enter fullscreen mode Exit fullscreen mode

That's crow-flies distance. For delivery or driving estimates, multiply by a road factor (typically 1.2-1.4 for urban areas, 1.1-1.2 for highways).

function estimateDrivingDistance(straightLine) {
  const roadFactor = 1.3; // Average for mixed urban/highway
  return {
    estimated: straightLine * roadFactor,
    low: straightLine * 1.1,
    high: straightLine * 1.5
  };
}
Enter fullscreen mode Exit fullscreen mode

Batch Distance Calculations

"Find the nearest store" is a common feature. Calculate distances to all locations and sort:

async function findNearestStores(userLocation, stores, limit = 5) {
  // Calculate distances in parallel
  const storesWithDistance = await Promise.all(
    stores.map(async store => {
      const distance = await getDistance(userLocation, {
        lat: store.latitude,
        lon: store.longitude
      });

      return {
        ...store,
        distance: distance.miles
      };
    })
  );

  // Sort by distance and return nearest
  return storesWithDistance
    .sort((a, b) => a.distance - b.distance)
    .slice(0, limit);
}

// Usage
const nearest = await findNearestStores(
  { lat: 40.7128, lon: -74.0060 }, // User in NYC
  allStores,
  3 // Top 3 nearest
);
// [
//   { name: "Manhattan Store", distance: 2.3 },
//   { name: "Brooklyn Store", distance: 4.1 },
//   { name: "Queens Store", distance: 7.8 }
// ]
Enter fullscreen mode Exit fullscreen mode

For large datasets (thousands of stores), calculate distances server-side and cache results by region. Don't make users wait for 5,000 API calls.

Geocoding: Addresses to Coordinates

Users enter addresses. APIs want coordinates. Geocoding bridges the gap.

async function geocodeAddress(address) {
  const res = await fetch(
    `https://api.apiverve.com/v1/geocoding?address=${encodeURIComponent(address)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    formattedAddress: data.formattedAddress,
    lat: data.latitude,
    lon: data.longitude,
    confidence: data.confidence,
    components: {
      street: data.street,
      city: data.city,
      state: data.state,
      country: data.country,
      postalCode: data.postalCode
    }
  };
}

// Usage
const location = await geocodeAddress("1600 Pennsylvania Ave, Washington DC");
// {
//   formattedAddress: "1600 Pennsylvania Avenue NW, Washington, DC 20500",
//   lat: 38.8977,
//   lon: -77.0365,
//   confidence: 0.98,
//   components: { ... }
// }
Enter fullscreen mode Exit fullscreen mode

The confidence score matters. Low confidence means the address might be ambiguous or partial—you may want to ask the user to confirm.

Reverse Geocoding: Coordinates to Addresses

User grants location permission. You get coordinates. Now what?

async function reverseGeocode(lat, lon) {
  const res = await fetch(
    `https://api.apiverve.com/v1/reversegeocode?lat=${lat}&lon=${lon}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    address: data.formattedAddress,
    street: data.street,
    city: data.city,
    state: data.state,
    country: data.country,
    postalCode: data.postalCode
  };
}

// Browser geolocation -> human-readable address
navigator.geolocation.getCurrentPosition(async (position) => {
  const location = await reverseGeocode(
    position.coords.latitude,
    position.coords.longitude
  );

  console.log(`You're near ${location.address}`);
});
Enter fullscreen mode Exit fullscreen mode

Great for "confirm your location" flows. Show users their detected address and let them correct if needed.

ZIP Code Lookup

Sometimes you just need city and state from a ZIP code:

async function lookupZipCode(zip) {
  const res = await fetch(
    `https://api.apiverve.com/v1/zipcodes?zipcode=${zip}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    zipCode: data.zipCode,
    city: data.city,
    state: data.state,
    stateCode: data.stateCode,
    county: data.county,
    timezone: data.timezone,
    coordinates: {
      lat: data.latitude,
      lon: data.longitude
    }
  };
}

// Auto-fill city/state from ZIP
const zipInput = document.querySelector('#zip');
zipInput.addEventListener('change', async (e) => {
  if (e.target.value.length === 5) {
    const location = await lookupZipCode(e.target.value);

    document.querySelector('#city').value = location.city;
    document.querySelector('#state').value = location.stateCode;
  }
});
Enter fullscreen mode Exit fullscreen mode

Auto-filling city and state from ZIP code is one of those tiny UX improvements that users notice and appreciate.

Timezone Handling

Time is hard. Timezones are harder. And daylight saving makes it worse.

async function getTimezone(lat, lon) {
  const res = await fetch(
    `https://api.apiverve.com/v1/timezonelookup?lat=${lat}&lon=${lon}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    timezone: data.timezone,           // "America/New_York"
    abbreviation: data.abbreviation,   // "EST" or "EDT"
    utcOffset: data.utcOffset,         // -5 or -4
    isDST: data.isDST,                 // true/false
    currentTime: data.currentTime
  };
}

// Convert time to user's timezone
function formatTimeForUser(utcTime, userTimezone) {
  return new Date(utcTime).toLocaleString('en-US', {
    timeZone: userTimezone,
    hour: 'numeric',
    minute: '2-digit',
    hour12: true
  });
}

// Usage
const meeting = "2026-01-24T15:00:00Z"; // 3pm UTC
const userTz = await getTimezone(40.7128, -74.0060); // NYC

formatTimeForUser(meeting, userTz.timezone);
// "10:00 AM" (EST)
Enter fullscreen mode Exit fullscreen mode

Building a Delivery Estimation System

Let's combine everything into something practical—delivery time estimates:

class DeliveryEstimator {
  constructor() {
    this.averageSpeed = 30; // mph in urban areas
  }

  async estimateDelivery(origin, destination) {
    // Get coordinates if addresses provided
    const [originCoords, destCoords] = await Promise.all([
      typeof origin === 'string' ? this.geocode(origin) : origin,
      typeof destination === 'string' ? this.geocode(destination) : destination
    ]);

    // Calculate distance
    const distance = await this.getDistance(originCoords, destCoords);

    // Estimate driving distance (straight line * road factor)
    const drivingDistance = distance.miles * 1.3;

    // Estimate time
    const drivingHours = drivingDistance / this.averageSpeed;
    const drivingMinutes = Math.round(drivingHours * 60);

    // Get timezone for destination (for delivery window display)
    const timezone = await this.getTimezone(destCoords.lat, destCoords.lon);

    // Calculate delivery window
    const now = new Date();
    const deliveryStart = new Date(now.getTime() + drivingMinutes * 60000);
    const deliveryEnd = new Date(deliveryStart.getTime() + 30 * 60000); // 30 min window

    return {
      distance: {
        straight: distance.miles,
        estimated: drivingDistance
      },
      time: {
        minutes: drivingMinutes,
        display: this.formatDuration(drivingMinutes)
      },
      deliveryWindow: {
        start: this.formatLocalTime(deliveryStart, timezone.timezone),
        end: this.formatLocalTime(deliveryEnd, timezone.timezone),
        timezone: timezone.abbreviation
      }
    };
  }

  async geocode(address) {
    const res = await fetch(
      `https://api.apiverve.com/v1/geocoding?address=${encodeURIComponent(address)}`,
      { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
    );
    const { data } = await res.json();
    return { lat: data.latitude, lon: data.longitude };
  }

  async getDistance(from, to) {
    const res = await fetch(
      `https://api.apiverve.com/v1/distancecalculator?lat1=${from.lat}&lon1=${from.lon}&lat2=${to.lat}&lon2=${to.lon}`,
      { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
    );
    return (await res.json()).data;
  }

  async getTimezone(lat, lon) {
    const res = await fetch(
      `https://api.apiverve.com/v1/timezonelookup?lat=${lat}&lon=${lon}`,
      { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
    );
    return (await res.json()).data;
  }

  formatDuration(minutes) {
    if (minutes < 60) return `${minutes} min`;
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
  }

  formatLocalTime(date, timezone) {
    return date.toLocaleString('en-US', {
      timeZone: timezone,
      hour: 'numeric',
      minute: '2-digit',
      hour12: true
    });
  }
}

// Usage
const estimator = new DeliveryEstimator();

const estimate = await estimator.estimateDelivery(
  "123 Warehouse St, Brooklyn NY",
  "456 Customer Ave, Manhattan NY"
);

// {
//   distance: { straight: 3.2, estimated: 4.16 },
//   time: { minutes: 8, display: "8 min" },
//   deliveryWindow: { start: "2:15 PM", end: "2:45 PM", timezone: "EST" }
// }
Enter fullscreen mode Exit fullscreen mode

Caching Location Data

Location lookups are perfect for caching—ZIP codes don't change, city coordinates don't move.

class LocationCache {
  constructor() {
    this.zipCache = new Map();
    this.geocodeCache = new Map();
    this.timezoneCache = new Map();
  }

  async getZipCode(zip) {
    if (this.zipCache.has(zip)) {
      return this.zipCache.get(zip);
    }

    const data = await lookupZipCode(zip);
    this.zipCache.set(zip, data);
    return data;
  }

  async geocode(address) {
    const key = address.toLowerCase().trim();

    if (this.geocodeCache.has(key)) {
      return this.geocodeCache.get(key);
    }

    const data = await geocodeAddress(address);
    this.geocodeCache.set(key, data);
    return data;
  }

  async getTimezone(lat, lon) {
    // Round to 2 decimal places for cache key (0.01 degree ≈ 1km)
    const key = `${lat.toFixed(2)},${lon.toFixed(2)}`;

    if (this.timezoneCache.has(key)) {
      return this.timezoneCache.get(key);
    }

    const data = await getTimezone(lat, lon);
    this.timezoneCache.set(key, data);
    return data;
  }
}
Enter fullscreen mode Exit fullscreen mode

ZIP codes can be cached forever. Geocoded addresses can be cached for days. Timezones can be cached with reduced precision (nearby points share timezones).

When You Actually Need Google Maps

Let's be honest about what these APIs don't do:

Turn-by-turn navigation. If you need actual driving directions with street names and turns, you need a routing API.

Real-time traffic. Crow-flies distance doesn't account for rush hour. For traffic-adjusted ETAs, you need traffic data.

Interactive maps. For displaying maps with pins and routes, you need a mapping library (though there are free/cheaper alternatives to Google Maps).

Street View. If you need imagery, that's proprietary.

For most location features—distance calculations, address validation, timezone handling, nearest-location queries—you don't need the Google tax.

The Cost Comparison

Let's do the math for that delivery app:

Google Maps pricing (2024):

  • Geocoding: $5 per 1,000 requests
  • Distance Matrix: $5-10 per 1,000 elements
  • Timezone: $5 per 1,000 requests

50,000 deliveries × 4 API calls average = 200,000 calls/month = ~$1,000-4,000/month

APIVerve pricing:

  • All location APIs: 1 credit each
  • Pro plan: {{plan.pro.price}}/month for {{plan.pro.calls}} credits

Same 200,000 calls = $400/month with aggressive caching bringing it down further.

The delivery app I mentioned? They needed full routing for a subset of deliveries (complex multi-stop routes). So they use Google for that specific use case (~5% of trips) and APIVerve for everything else. Monthly savings: $3,000+.


Location features don't have to be expensive. Distance calculations, geocoding, timezone lookups—these are solved problems. You don't need enterprise pricing for basic math.

The Distance Calculator, Geocoding, Reverse Geocode, ZIP Code Lookup, and Timezone Lookup APIs handle the common cases. Same API key, consistent responses.

Get your API key and build location features without the enterprise bill.


Originally published at APIVerve Blog

Top comments (0)