Measuring Area Accurately on OSM Polygons Jump to heading
Area is the measurement people get wrong most often and notice least, because every wrong answer is a plausible number. A forest measured in Web Mercator at sixty degrees north is four times too large, and nothing about the figure looks unusual.
Prerequisites Jump to heading
Conceptual minimum Jump to heading
OSM stores geographic coordinates, and degrees are not a unit of distance. A square degree near the equator covers roughly twelve thousand square kilometres; at sixty degrees north it covers about six thousand, because lines of longitude converge. Computing area from degree coordinates therefore produces a number whose unit varies with latitude.
Web Mercator is worse, not better. It is a conformal projection, preserving angles at the cost of area, and its area distortion grows as the square of the secant of the latitude. At sixty degrees it inflates area by a factor of four; at seventy-five, by about fifteen. Because Web Mercator is the projection everything renders in, it is the one people reach for, and it is the least suitable for measurement of any in common use.
Two approaches give correct answers.
An equal-area projection — a local one such as a Lambert Azimuthal Equal Area centred on the feature, or a regional standard — preserves area by construction. Accuracy is excellent for features within a few hundred kilometres of the projection centre.
A geodesic computation measures on the ellipsoid directly, with no projection at all. It is correct at any size and any location, marginally slower, and the right default when features are scattered.
Runnable solution Jump to heading
from __future__ import annotations
import logging
import math
from dataclasses import dataclass
from pyproj import Geod, Transformer
from shapely.geometry import Polygon
from shapely.geometry.base import BaseGeometry
from shapely.ops import transform
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger("osm.crs.area")
GEOD = Geod(ellps="WGS84")
LOCAL_LIMIT_KM = 500.0 # beyond this, a local projection loses accuracy
@dataclass(frozen=True)
class AreaResult:
square_metres: float
method: str
note: str = ""
def geodesic_area(geom: BaseGeometry) -> AreaResult:
"""Measure on the ellipsoid. Correct at any size, anywhere."""
if geom.geom_type == "MultiPolygon":
total = sum(geodesic_area(p).square_metres for p in geom.geoms)
return AreaResult(total, "geodesic", "summed over parts")
if geom.geom_type != "Polygon":
raise TypeError(f"area is undefined for {geom.geom_type}")
lons, lats = zip(*geom.exterior.coords)
area, _perimeter = GEOD.polygon_area_perimeter(lons, lats)
total = abs(area)
for ring in geom.interiors:
hole_lons, hole_lats = zip(*ring.coords)
hole_area, _ = GEOD.polygon_area_perimeter(hole_lons, hole_lats)
total -= abs(hole_area)
return AreaResult(total, "geodesic")
def equal_area_projection_area(geom: BaseGeometry) -> AreaResult:
"""Project onto an equal-area plane centred on the feature, then measure."""
centre = geom.centroid
# Lambert Azimuthal Equal Area centred on the feature itself: distortion
# is zero at the centre and grows slowly with distance from it.
proj = (f"+proj=laea +lat_0={centre.y} +lon_0={centre.x} "
f"+x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs")
transformer = Transformer.from_crs("EPSG:4326", proj, always_xy=True)
projected = transform(transformer.transform, geom)
return AreaResult(projected.area, "equal-area projection")
def extent_km(geom: BaseGeometry) -> float:
minx, miny, maxx, maxy = geom.bounds
_, _, diagonal = GEOD.inv(minx, miny, maxx, maxy)
return diagonal / 1000.0
def area_of(geom: BaseGeometry) -> AreaResult:
"""Pick a method from the feature's extent, and say which was used."""
if geom.is_empty:
return AreaResult(0.0, "none", "empty geometry")
if not geom.is_valid:
# An invalid ring has no well-defined area; do not guess one.
raise ValueError("geometry is invalid; repair before measuring")
span = extent_km(geom)
if span > LOCAL_LIMIT_KM:
result = geodesic_area(geom)
return AreaResult(result.square_metres, result.method,
f"extent {span:.0f} km exceeds the local-projection limit")
return equal_area_projection_area(geom)
def mercator_error_factor(latitude_deg: float) -> float:
"""How much a Web Mercator area calculation would overstate, here."""
return 1.0 / (math.cos(math.radians(latitude_deg)) ** 2)
if __name__ == "__main__":
square = Polygon([(19.9, 50.0), (20.0, 50.0), (20.0, 50.1), (19.9, 50.1)])
for result in (area_of(square), geodesic_area(square)):
logger.info("%-26s %12.1f m2 %s", result.method,
result.square_metres, result.note)
logger.info("Web Mercator here would overstate by %.2fx",
mercator_error_factor(50.0))
Step-by-step walkthrough Jump to heading
- Refuse to measure invalid geometry. A self-intersecting ring has no well-defined area, and the number a library returns for one is an artefact of the algorithm rather than a measurement.
- Subtract holes explicitly in the geodesic path. The ellipsoid area function measures a single ring, so interior rings must be computed and subtracted rather than assumed handled.
- Take absolute values. The sign of a geodesic ring area depends on winding direction, and OSM ring orientation is not guaranteed.
- Centre the projection on the feature. An equal-area projection has zero distortion at its centre, so centring it per feature keeps accuracy high without needing a regional standard.
- Switch on extent, not on location. A feature spanning more than a few hundred kilometres accumulates error in any local projection, and the geodesic path has no such limit.
- Report the method. A stored area whose method is unknown cannot be compared against one computed differently, and the two can differ by a fraction of a percent, which matters when the numbers are summed.
- Keep the Mercator factor available. Being able to say by how much a wrong calculation would have been wrong is what convinces somebody that the existing figures need recomputing.
Verification Jump to heading
- A known area matches. Take a feature whose area is published independently and confirm agreement within a fraction of a percent.
- The two correct methods agree. For a moderate feature, the local projection and the geodesic computation should differ negligibly.
- Latitude does not change the answer. Move a test polygon of fixed geodesic size to several latitudes; the computed area should stay constant.
- Holes are subtracted. A polygon with a hole must measure less than the same outline without it.
- Invalid geometry raises. Feed a bowtie and confirm an error rather than a number.
Common errors and fixes Jump to heading
| Symptom | Root cause | One-line fix |
|---|---|---|
| Areas grow with latitude | Computed in Web Mercator | Use an equal-area projection or a geodesic method |
| Areas in the thousandths | Computed in degrees | Project or measure geodetically before reporting |
| Holes not subtracted | Only the exterior ring measured | Compute and subtract each interior ring |
| Negative areas | Ring winding taken as meaningful | Take absolute values; OSM winding is not guaranteed |
| Large features slightly off | Local projection used beyond its range | Switch to geodesic above a few hundred kilometres |
| Stored areas incomparable | Method not recorded | Record which method produced each figure |
| Nonsense area on a bowtie | Invalid geometry measured anyway | Validate before measuring and refuse invalid input |
Specification reference Jump to heading
Web Mercator preserves angles and inflates area by a factor equal to the square of the secant of the latitude, which reaches four at sixty degrees. Equal-area projections such as Lambert Azimuthal Equal Area preserve area exactly at the cost of shape, and geodesic polygon area computation on a reference ellipsoid is independent of any projection. See the pyproj Geod documentation for the ellipsoidal computation and Coordinate Reference Systems in OSM for the projection background.
Frequently Asked Questions Jump to heading
Why is Web Mercator so bad for area?
Because it is conformal: it preserves local angles, and the price of that is that scale varies with latitude. Area, being scale squared, is inflated by the square of the secant of the latitude — a factor of two at forty-five degrees and four at sixty. It is also the projection everything renders in, so it is the one already loaded in most pipelines, which is exactly why the mistake is so common.
Should I use an equal-area projection or a geodesic computation?
Either is correct for ordinary features, and they agree to well within a percent. A local equal-area projection centred on the feature is slightly faster and entirely adequate up to a few hundred kilometres of extent. A geodesic computation has no size limit and needs no per-feature projection setup, which makes it the simpler default when features are scattered across a continent.
Can I just apply a correction factor to Mercator areas?
Only for a single point, which is rarely what you have. The factor depends on latitude, so a polygon spanning several degrees has a different error at its north and south edges, and a dataset spanning a country has errors differing by tens of percent between its extremes. A single correction is exact nowhere except at the latitude it was derived for.
Does ring winding direction matter?
For the geodesic computation, yes, in that it determines the sign of the result — which is why absolute values are taken. OSM does not guarantee a winding direction on multipolygon rings, and the assembly discussed in Handling Multipolygon Members with No Role derives containment geometrically rather than from orientation, so the sign carries no information worth preserving.
Related Jump to heading
- Coordinate Reference Systems in OSM — the parent topic and the projection model.
- Converting OSM Coordinates to a Local CRS with pyproj — the transformation this builds on.
- Picking a UTM Zone for an OSM Extract — a regional alternative to a per-feature projection.
- Handling Antimeridian-Crossing OSM Geometry — the other measurement trap in geographic coordinates.
- Merging Adjacent OSM Polygons for Low Zoom — where an area threshold depends on this being right.
Up one level: Coordinate Reference Systems in OSM.