Degrees Are Not Metres

SRID, ST_SetSRID, ST_Transform — and the trap that never errors

What this lesson covers

  • What an SRID actually is
  • The query everyone writes first
  • Three honest fixes: geography, UTM, Web Mercator
  • Why two of them agree and one inflates
  • The PROJ setting that fails without an error

Coordinates in SRID 4326 are angles, not lengths. Every function that returns a distance or an area will still answer — it just answers in degrees, and no error says so.

What an SRID actually is

An SRID is not a setting. It is a row in a tablespatial_ref_sys, which ships with PostGIS and holds a few thousand coordinate systems, each with a full description of how its numbers map onto the Earth.

Three of them matter here:

SRID What it is Coordinates in
4326 WGS 84, plain longitude/latitude — no projection at all degrees
3857 Web Mercator, what slippy map tiles use metres
32654 UTM zone 54N, the zone Tokyo sits in metres

Only the first has no PROJCS entry, because nothing has been projected: 4326 is the globe, described by angles. The other two flatten it, and flattening is where the arithmetic starts working.

That single fact drives the whole lesson. ST_Area does not know or care what your numbers mean — it multiplies them. Give it degrees and it returns square degrees, a unit that has no fixed size on the ground and shrinks as you move away from the equator.

An SRID is a row in a table

SELECT srid,
       auth_name || ':' || auth_srid                      AS authority,
       substring(srtext from 'PROJCS\["([^"]+)"')         AS projected_as,
       substring(srtext from 'GEOGCS\["([^"]+)"')         AS built_on,
       (regexp_match(srtext, '.*UNIT\["([^"]+)"'))[1]     AS coordinates_are_in
FROM spatial_ref_sys
WHERE srid IN (4326, 3857, 32654)
ORDER BY srid;

PostGIS ships spatial_ref_sys, and every SRID you have ever pasted into a query is a row in it. Pulling three apart shows what the number stands for: 4326 has no PROJCS at all, because nothing is projected — it is the globe in degrees. The other two are projections built on the same datum, and both measure in metres.

Run this query yourself

The query everyone writes first

SELECT name,
       ST_Area(geom) AS area
FROM osm.building_polygon
WHERE name IS NOT NULL AND name <> ''
  AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326)
ORDER BY area DESC
LIMIT 20;

Twenty named buildings around Tokyo Station, ordered by area — the query anyone writes on their first day. ST_Area accepts the geometry, returns a number, sorts correctly and errors on nothing. The values are tiny, which reads as "very precise" rather than "wrong unit", and the ordering is genuinely correct. Everything about the result encourages you to believe it.

Run this query yourself

name sq_degrees m2_geography m2_utm54n m2_mercator
東京駅一番街 0.0000017505 17582.5 17573.8 26706.1
宮殿 0.0000016948 17021.9 17013.6 25855.5
日本銀行 0.0000014574 14637.0 14629.8 22235.2

Real rows from osm.building_polygon (Tokyo Station bbox), PostGIS 3.6.1 / PROJ 9.8.1, 2026-08-22. These are exactly what the lesson's query returns in the editor.

Why a degree is not a distance

A degree of latitude is very nearly constant — about 111 km anywhere on Earth. A degree of longitude is not: it is 111 km at the equator, 78 km in Tokyo, and zero at the poles.

That is the whole problem. ST_Area on a 4326 geometry multiplies one by the other, so its result is neither an area nor a constant fraction of one — it shrinks as you move away from the equator. Two identical buildings, one in Jakarta and one in Reykjavík, return different "areas".

A degree of latitude is constant; a degree of longitude shrinks towards the poles

One degree east is not one degree north — and neither is a metre.

Three honest fixes, and one that lies

Once you accept that 4326 coordinates are angles, there are three ways to get a real measurement — and they do not agree with each other by accident.

ST_Area(geom::geography)             -- 1. treat it as a sphere, answer in m²
ST_Area(ST_Transform(geom, 32654))   -- 2. project to the local UTM zone, answer in m²
ST_Area(ST_Transform(geom, 3857))    -- 3. project to Web Mercator, answer in "m²"

::geography does spherical arithmetic on the ellipsoid. No projection, no zone to pick, correct anywhere on Earth, and slower than the alternatives. It is the right default.

UTM is a projection designed to be accurate over a narrow strip — sixty zones, six degrees of longitude each. Inside its own zone it is excellent; drag a query across a zone boundary and it degrades. Tokyo is zone 54N.

Web Mercator is designed for drawing map tiles, not measuring. It preserves angles and inflates area with latitude. That inflation is not a rounding error: at Tokyo's latitude it is a factor of about 1.52, and it grows the further north you go.

So two of the three agree to within a rounding error and the third is half again too big. If your area is coming back suspiciously large and your data went through 3857 at some point, you have found it.

Measure properly — three ways, side by side

SELECT name,
       ST_Area(geom)                       AS sq_degrees,
       ST_Area(geom::geography)            AS m2_geography,
       ST_Area(ST_Transform(geom, 32654))  AS m2_utm54n,
       ST_Area(ST_Transform(geom, 3857))   AS m2_mercator
FROM osm.building_polygon
WHERE name IS NOT NULL AND name <> ''
  AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326)
ORDER BY m2_geography DESC
LIMIT 20;

The same twenty buildings measured four ways at once: raw degrees, then the three honest answers. Reading across a row is the entire lesson — geography and UTM agree to about one part in two thousand, Web Mercator is half again larger, and the first column is not a unit of area at all.

Run this query yourself

Why two agree and one inflates

SELECT name,
       round(ST_Area(geom::geography))                                        AS m2_geography,
       round(ST_Area(ST_Transform(geom, 32654)))                              AS m2_utm54n,
       round(ST_Area(ST_Transform(geom, 3857)))                               AS m2_mercator,
       round((ST_Area(ST_Transform(geom, 32654)) / ST_Area(geom::geography))::numeric, 4) AS utm_ratio,
       round((ST_Area(ST_Transform(geom, 3857))  / ST_Area(geom::geography))::numeric, 4) AS mercator_ratio
FROM osm.building_polygon
WHERE name IS NOT NULL AND name <> ''
  AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326)
ORDER BY m2_geography DESC
LIMIT 20;

The same three measurements, divided by the spherical answer so the disagreement becomes a ratio. UTM lands within a fraction of a percent — it is designed to be accurate in its own zone, and Tokyo is in zone 54N. Web Mercator comes back around 1.52, and that factor is not noise: it is a property of the projection at this latitude, growing as you move north.

Run this query yourself

The buildings you have been measuring

SELECT name,
       round(ST_Area(geom::geography)) AS m2,
       geom
FROM osm.building_polygon
WHERE name IS NOT NULL AND name <> ''
  AND geom && ST_MakeEnvelope(139.75, 35.67, 139.78, 35.69, 4326)
ORDER BY m2 DESC
LIMIT 40;

The same forty buildings with their true areas in square metres, returned as geometry so you can see what the numbers describe. The largest is a station complex; the smallest are kiosks. Areas that looked like 0.0000017 a moment ago are 17,582 m² — the shapes did not change, only the question.

Run this query yourself

The setting that fails without an error

Reprojection is not one algorithm. For a datum change, PROJ may need a grid shift file — a correction surface describing how one datum's coordinates map to another's. Those files are large, versioned and distributed separately from the library.

This server does not fetch them:

PROJ 9.4.0  NETWORK_ENABLED=OFF  URL_ENDPOINT=https://cdn.proj.org  DATABASE_PATH=/usr/share/proj/proj.db

NETWORK_ENABLED=OFF means PROJ will never reach cdn.proj.org. When a transform needs a grid it does not have, it does not stop — it falls back to a lower-accuracy path and returns numbers anyway. No error, no warning, just answers that are a metre or two out.

For everything in this course that is harmless: 4326, 3857 and UTM all sit on the WGS 84 datum, so no grid is involved. It matters the moment you bring in national datums — NAD27, OSGB36, Tokyo Datum — where the difference between the correct grid and the fallback can be hundreds of metres.

Check the build before you trust a datum shift. The version string is the whole story, and it is one query away.

The PROJ build behind ST_Transform

SELECT PostGIS_PROJ_Version()                                              AS proj_build,
       substring(PostGIS_PROJ_Version() from 'NETWORK_ENABLED=(\w+)')      AS network,
       substring(PostGIS_PROJ_Version() from 'DATABASE_PATH=(\S+)')        AS proj_db,
       PostGIS_Lib_Version()                                               AS postgis;

Every ST_Transform you write is executed by PROJ, and its build string says what it can and cannot do. NETWORK_ENABLED=OFF means it will never download a grid-shift file, so a datum transform that needs one silently takes a less accurate route instead of failing. Harmless for everything here — 4326, 3857 and UTM share the WGS 84 datum — and important the day you bring in a national one.

Run this query yourself

Rules of thumb

  • ST_SetSRID relabels. ST_Transform reprojects. They are not interchangeable.
  • Cast to ::geography to measure 4326 data — the answer is in metres, anywhere on Earth.
  • Never measure in 3857. At Tokyo's latitude it inflates area by 1.52×.
  • Check PostGIS_PROJ_Version() before trusting a datum shift — network off means silent fallbacks.

Run these queries against a live PostGIS. Open the lesson in the app — the same SQL, an editor, and your own data.

Geometry Is a Type, Not Two Numbers · Reading a Schema You Did Not Write → · All lessons