Chibuzor Daniel Uwezukwe

How We Calculate Delivery Zones Using the Haversine Formula

2026-08-07

When we built Storich, one of the earliest product decisions was: how do we know if a customer is within our delivery zone? We could not afford to buy a premium geocoding API for every address lookup, and we needed something deterministic, fast, and cheap to run. The answer was the Haversine formula.

What is the Haversine Formula?

The Haversine formula calculates the shortest distance between two points on a sphere given their latitude and longitude. It accounts for the Earth's curvature, something a simple Euclidean distance calculation ignores, which gives wrong results at any meaningful scale.

The formula:

a = sin²(Δlat/2) + cos(lat1) · cos(lat2) · sin²(Δlon/2)
c = 2 · atan2(√a, √(1−a))
d = R · c

Where R is Earth's radius (6,371 km).

Implementation in TypeScript

function haversineDistance(
  lat1: number,
  lon1: number,
  lat2: number,
  lon2: number,
): number {
  const R = 6371; // Earth radius in km
  const toRad = (deg: number) => (deg * Math.PI) / 180;

  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);

  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

How Storich Uses It

Each delivery zone in our database has a center coordinate and a radius:

@Entity()
export class DeliveryZone {
  @Column('decimal') centerLat: number;
  @Column('decimal') centerLon: number;
  @Column('decimal') radiusKm: number;
  @Column('decimal') deliveryFee: string;
  @Column() estimatedEtaMinutes: number;
}

When a customer enters their address, we reverse-geocode the coordinates via OpenStreetMap Nominatim to get a human-readable label, then run the Haversine check against every active zone to find a match:

async function findDeliveryZone(
  customerLat: number,
  customerLon: number,
  zones: DeliveryZone[],
): Promise<DeliveryZone | null> {
  for (const zone of zones) {
    const distance = haversineDistance(
      customerLat,
      customerLon,
      Number(zone.centerLat),
      Number(zone.centerLon),
    );

    if (distance <= zone.radiusKm) {
      return zone;
    }
  }
  return null; // outside all delivery zones
}

If no zone matches, the customer sees an "outside delivery area" message rather than a broken checkout.

Why Not Just Use PostGIS?

PostGIS is the right answer at scale, it has spatial indexes and handles complex polygon coverage. But for an early-stage product launching in one city with a handful of zones, it adds infrastructure complexity for no real benefit. Haversine running in application code against a small dataset is fast enough (sub-millisecond) and requires zero additional setup. We can migrate to PostGIS when zone count or query volume demands it.

Key Takeaway

The Haversine formula looks intimidating written as a math equation but is trivially simple to implement and extremely useful for any location-aware feature. If you are building delivery, logistics, or proximity search and your dataset is small, reach for Haversine before spinning up a spatial database.