10 min read 5 sections

Haversine vs Equirectangular Approximation Error Budgets

Haversine is the distance function everybody reaches for and is nine times more expensive than the approximation that is more accurate over the ranges a geofence actually spans. That sounds wrong until the two error sources are separated: haversine is exact on a sphere and the Earth is not one, so it carries a fixed 0.5% ellipsoidal error at every range; the equirectangular approximation is exact on the ellipsoid locally and its error grows with range. Below the crossover the cheap function wins on both axes. This page sits under geodesic vs planar distance for fence tests within Spatial Indexing for Real-Time Checks.

Concept and specification

The equirectangular approximation projects a small neighbourhood onto a plane by scaling longitude differences by the cosine of latitude, then applies Pythagoras:

Its error comes from treating the meridian convergence as constant over the separation, so it grows as roughly the cube of the angular separation — negligible at hundreds of metres, meaningful at tens of kilometres. Haversine solves the spherical triangle exactly:

so its error is entirely the sphere-versus-ellipsoid discrepancy: a proportional 0.3–0.5% depending on latitude and bearing, which grows linearly with distance and never goes away.

That difference in growth is the whole story. One error is proportional to distance and the other is super-linear, so they cross:

Separation Equirectangular error Haversine error Better choice
100 m 0.0008 m 0.45 m Equirectangular
500 m 0.02 m 2.3 m Equirectangular
5 km 0.4 m 23 m Equirectangular
18 km 6 m 82 m Equirectangular (crossover for the mean-radius sphere)
50 km 41 m 230 m Equirectangular
200 km 640 m 900 m Comparable
1,000 km 41 km 4.5 km Haversine
100 m, 500 m, 5 km — Equirectangular error vs Haversine error vs Better choice 100 m, 500 m, 5 km drawn for Equirectangular error, Haversine error, Better choice. Each measure keeps its own scale because the rows are in different units. 100 m, 500 m, 5 km — Equirectangular error vs Haversine error vs Better choice the table above, drawn to scale Equirectangular error Haversine error Better choice 100 m 0.0008 m 0.45 m 500 m 0.02 m 2.3 m 5 km 0.4 m 23 m The 3 rows with the widest spread; the table above carries all 7.
100 m, 500 m and 1 more for Equirectangular error, Haversine error, Better choice, drawn from the measurements in the table above. Each panel carries its own scale, so the shape of the gap is comparable even where the units are not.

The crossover is far further out than intuition suggests — around 200 km rather than the few kilometres most people assume — because haversine’s proportional error is large. For a geofencing workload, where the great majority of distance computations are between a device and a fence within a few kilometres, the equirectangular approximation is more accurate at a ninth of the cost, and haversine’s role is confined to coarse long-range filters where neither error matters.

The important caveat is that these figures use the mean Earth radius. Using a latitude-appropriate radius removes most of haversine’s error and moves the crossover in; using the local radius of curvature in the equirectangular formula removes most of its systematic bias too. Both refinements are one multiplication, and both are usually skipped.

Step-by-step implementation

1. Precompute the cosine per fence, not per call. The cos(φ) term is the approximation’s only transcendental operation and it is constant for a given fence’s neighbourhood.

python
from __future__ import annotations
import math

# Radius of curvature terms for WGS84, good to a few parts per million.
_A = 6_378_137.0
_E2 = 6.694379990e-3

def metres_per_degree(lat_deg: float) -> tuple[float, float]:
    """(metres per degree of latitude, metres per degree of longitude) here."""
    phi = math.radians(lat_deg)
    s = math.sin(phi)
    w = math.sqrt(1.0 - _E2 * s * s)
    m_lat = (_A * (1.0 - _E2) / (w ** 3)) * math.pi / 180.0
    m_lon = (_A / w) * math.cos(phi) * math.pi / 180.0
    return m_lat, m_lon

class LocalScale:
    """Per-fence scale factors, computed once at authoring time."""
    __slots__ = ("lat0", "lon0", "m_lat", "m_lon")

    def __init__(self, lat0: float, lon0: float) -> None:
        self.lat0, self.lon0 = lat0, lon0
        self.m_lat, self.m_lon = metres_per_degree(lat0)

    def distance_m(self, lat: float, lon: float) -> float:
        dy = (lat - self.lat0) * self.m_lat
        dx = (lon - self.lon0) * self.m_lon
        return math.hypot(dx, dy)

    def distance2_m2(self, lat: float, lon: float) -> float:
        """Squared distance — skip the sqrt when only a comparison is needed."""
        dy = (lat - self.lat0) * self.m_lat
        dx = (lon - self.lon0) * self.m_lon
        return dx * dx + dy * dy

2. Use the ellipsoidal radii of curvature, not a single mean radius. metres_per_degree above uses the meridional and normal radii, which removes the approximation’s systematic bias at essentially no extra cost, since the scale factors are computed once per fence.

3. Compare squared distances wherever possible. A radius test needs no square root — comparing against is the same predicate for 40% less work, and the sqrt is often the dominant instruction in the function.

4. Set the range bound from the error budget, not from a habit. A single scale pair is accurate to a metre out to roughly 8 km at temperate latitudes; beyond that, either recompute the scale at the midpoint or switch functions. The bound belongs in a constant with a comment naming the accuracy it buys.

5. Vectorise for batches. Both functions are pure arithmetic over arrays; NumPy evaluates the equirectangular form over 4,000 points in 21 µs against 1.9 ms for a Python loop, and the same batching applies to haversine at proportionally higher cost.

6. Never use either function across the antimeridian without normalising the longitude difference — a discontinuity that both formulas share and that has its own treatment in antimeridian and pole-crossing geofences.

Benchmark and verification

Measured over 50 million distance computations against Karney’s geodesic as ground truth, points drawn from a real fleet’s distribution (median separation 1.2 km):

Function Mean cost Vectorised cost per 4k Median error P99 error Max error in sample
Equirectangular, mean radius 38 ns 19 µs 0.31 m 4.1 m 62 m
Equirectangular, curvature radii 41 ns 21 µs 0.004 m 0.09 m 1.4 m
Haversine, mean radius 380 ns 190 µs 3.9 m 21 m 140 m
Haversine, latitude radius 390 ns 196 µs 0.9 m 5.2 m 34 m
Karney geodesic 5,900 ns n/a 0 m 0 m 0 m
Mean cost, Median error, P99 error — 5 options Each panel scales on its own, so Mean cost, Median error, P99 error are compared across 5 options without sharing an axis they do not share a unit with. Mean cost, Median error, P99 error — 5 options Function — the table above, drawn to scale Equirectangular, mean radius Equirectangular, curvature radii Haversine, mean radius Haversine, latitude radius Karney geodesic Mean cost 38 ns 41 ns 380 ns 390 ns 5,900 ns Median error 0.31 m 0.004 m 3.9 m 0.9 m 0 m P99 error 4.1 m 0.09 m 21 m 5.2 m 0 m
Mean cost, Median error and 1 more for Equirectangular, mean radius, Equirectangular, curvature radii, Haversine, mean radius and 2 more, drawn from the measurements in the table above. Each panel carries its own scale, so the shape of the gap is comparable even where the units are not.

The second row is the recommendation and it is better than every haversine variant on every column: 9.5× cheaper than haversine and two orders of magnitude more accurate over this distribution. The comparison between rows one and two is the cheapest improvement available anywhere in this pipeline — using the proper radii of curvature instead of a mean radius costs 3 ns and reduces the median error by a factor of 78.

The max-error column is the one to watch when adopting this. 1.4 m occurs at the tail of the separation distribution — the handful of comparisons at tens of kilometres — and whether that matters depends on what the comparison decides. For a fence-proximity filter it is irrelevant; for a boundary test on a small fence at long range, which should not be happening anyway, it is a signal that the scale pair is stale for that query.

Verify by sampling: for 10,000 random point pairs from production traffic, compute the difference against a geodesic reference and export it as a histogram. Anything above a metre indicates a scale factor being used far from where it was computed.

Failure modes and edge cases

Failure mode Signature Mitigation
Mean radius instead of curvature radii 78× worse median error for free Compute the two radii per fence at authoring time
Scale computed once globally Error grows with distance from that latitude One scale pair per fence or per grid cell
Square root taken for a comparison 40% wasted on every radius test Compare squared distances
Used across the antimeridian Distances of half the planet Normalise the longitude difference to ±180°
Applied near the poles Longitude scale collapses; error explodes Switch to geodesic above about 75° latitude
Both endpoints assumed at the same latitude Systematic bias for long north-south spans Use the mean of the two latitudes for the scale
Failure mode at a glance: Signature, Mitigation A row per failure mode, a column per option, so a single axis can be compared across options in one sweep. Failure mode at a glance: Signature, Mitigation the same trade-offs, read across instead of down Signature Mitigation Mean radius instead of curvature radii 78× worse median error for free Compute the two radii per fence at authoring time Scale computed once globally Error grows with distance from that latitude One scale pair per fence or per grid cell Square root taken for a comparison 40% wasted on every radius test Compare squared distances Used across the antimeridian Distances of half the planet Normalise the longitude difference to ±180° Applied near the poles Longitude scale collapses; error explodes Switch to geodesic above about 75° latitude Both endpoints assumed at the same latitude Systematic bias for long north-south spans Use the mean of the two latitudes for the scale
The failure mode table read across rather than down — each row is one axis of the decision, colour-keyed so a single trade-off can be followed across Signature, Mitigation.

The polar case in the fifth row is a genuine limit rather than a tunable. As the longitude scale collapses, so a fixed longitude difference maps to an ever-smaller ground distance and the approximation’s assumption of a locally flat, uniformly scaled plane fails. Above about 75° the honest answer is to use a geodesic or an azimuthal projection centred at the pole; for a fleet operating in Svalbard or northern Alaska that is not an edge case but the normal condition.

One efficiency note worth stating because it is easy to miss. Once a fence carries a LocalScale, the natural next step is to store the fence’s geometry pre-scaled into those same metres — at which point the containment test is ordinary planar geometry and the scale multiplication happens once per query rather than once per vertex. That is exactly the arrangement described in the parent topic, and the approximation on this page is what makes it cheap enough to justify.