PostGIS Geography vs. Geometry: Choosing the Right Type for Your Spatial Data
If you’ve ever installed PostGIS and opened the documentation, you’ve run into the type decision right away: geometry or geography? They look similar, they both store spatial coordinates, and they share many function names. The difference matters more than it first appears. Choosing the wrong one leads to silently incorrect distance calculations.
Why Two Types Exist
PostGIS was created at Refractions Research in 2001 and released as open source shortly after. The geometry type came first, implementing the OGC Simple Features specification, which models spatial objects on a flat, two-dimensional Cartesian plane. That design serves a large class of GIS problems well, particularly when you’re working within a single region using a projected coordinate system like UTM, where the native unit is meters and the distortion across the area of interest is negligible.
The geography type arrived in PostGIS 1.5 around 2010. It was added specifically to address the accuracy problems that accumulate when you treat the curved surface of the Earth as if it were flat. The farther apart two points are, the more the flat-plane approximation diverges from reality. For a team computing distances between cities on different continents, geometry with SRID 4326 gives wrong answers. The geography type solves this by performing its arithmetic on a spheroid model of the Earth instead.
What’s Different Under the Hood
The geometry type stores coordinates in whatever spatial reference system you declare with an SRID. When you use SRID 4326, those coordinates are longitude and latitude in decimal degrees, and distance calculations return a value in the native unit of that coordinate system, which is degrees. A degree of longitude represents about 111 km at the equator but shrinks to zero at the poles. Degrees are good for locating places, but are not a stable unit for measuring distance between two places.
The geography type stores coordinates as longitude/latitude pairs, but instead of treating them as points on a flat grid, it models them on a more accurate WGS84 spheroid, the same reference frame used by GPS. All distance calculations return meters. The math underneath is more expensive: computing geodesic distances requires solving the inverse geodetic problem rather than simple Pythagorean math. PostGIS uses Karney’s algorithm for this, which provides millimeter accuracy at any point on Earth.
The Difference Is Not Just Academic
Let’s look at the distance between two airports: LAX in Los Angeles and RDU between Raleigh and Durham, North Carolina.
WITH coords AS (
SELECT
-118.4081 AS lax_lon,
33.9425 AS lax_lat,
-78.7875 AS rdu_lon,
35.8776 AS rdu_lat
)
SELECT
ST_Distance(
ST_SetSRID(ST_MakePoint(lax_lon, lax_lat), 4326),
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326)
) AS geometry_distance_degrees,
ST_Distance(
ST_SetSRID(ST_MakePoint(lax_lon, lax_lat), 4326)::geography,
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326)::geography
) / 1609.344 AS geography_distance_miles
FROM coords;
geometry_distance_degrees | geography_distance_miles
---------------------------+--------------------------
39.66782772436626 | 2238.4628141622798
The geometry result is 39.67, in degrees. The geography result is just under 2,240 miles. Both queries describe the separation between the airport coordinates, but geometry measures it on a plane while geography measures it on an ellipsoid.
The unit problem becomes concrete when you try to write a proximity query against a global dataset. A developer unfamiliar with the pitfalls might attempt to convert the degrees to miles. Using a simple latitude-based approximation within a 2-D plane that one degree is about 69 miles, they divide 5,000 by 69 and pass 5000.0 / 69.0 as the distance threshold.
Create the table of sample airports, with location stored as a geometry field.
CREATE TABLE airports (code text, name text, location geometry(Point, 4326));
INSERT INTO airports VALUES
('LAX', 'Los Angeles Intl', ST_GeomFromText('POINT(-118.4081 33.9425)', 4326)),
('RDU', 'Raleigh-Durham Intl', ST_GeomFromText('POINT(-78.7875 35.8776)', 4326)),
('ANC', 'Ted Stevens Anchorage', ST_GeomFromText('POINT(-149.9961 61.1744)', 4326)),
('LHR', 'London Heathrow', ST_GeomFromText('POINT(-0.4543 51.4775)', 4326)),
('EZE', 'Buenos Aires Ezeiza', ST_GeomFromText('POINT(-58.5358 -34.8222)', 4326)),
('SVO', 'Moscow Sheremetyevo', ST_GeomFromText('POINT(37.4146 55.9726)', 4326)),
('LOS', 'Lagos Murtala Muhammed',ST_GeomFromText('POINT(3.3211 6.5774)', 4326)),
('SYD', 'Sydney Kingsford Smith',ST_GeomFromText('POINT(151.1772 -33.9461)', 4326));
Now query for airports within 5,000 miles of RDU using the degree approximation:
-- Attempting to find airports within 5,000 miles of RDU
-- using the "1 degree ≈ 69 miles" approximation
WITH search AS (
SELECT
-78.7875 AS rdu_lon,
35.8776 AS rdu_lat,
5000.0 / 69.0 AS radius_degrees
)
SELECT code, name
FROM airports, search
WHERE ST_DWithin(
location,
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326),
radius_degrees
);
code | name
------+----------------------
LAX | Los Angeles Intl
RDU | Raleigh-Durham Intl
ANC and LHR are both missing. Anchorage is roughly 3,480 miles from Raleigh and London is roughly 3,870 miles away — both well within the 5,000-mile target. The degree approximation drops them for two different reasons.
Anchorage sits at 61°N. At that latitude, one degree of longitude represents only about 33 miles, not 69. The large east-west gap between ANC and RDU (about 71 degrees of longitude) produces a very large planar degree distance, inflating the computed distance far past the threshold even though the real-world flight is under 5,000 miles.
London sits at a more moderate latitude, but to reach it from Raleigh you cross the Atlantic along a curved geodesic path. The geometry calculation treats the coordinate difference as a straight line on a flat grid, which significantly overstates the true distance.
A developer who understands the longitude shrinkage at higher latitudes might try to correct for it. At RDU (35.88°N), one degree of longitude is cos(35.88° × π/180) × 69 ≈ 55.97 miles, so you could divide by that instead:
WITH search AS (
SELECT
-78.7875 AS rdu_lon,
35.8776 AS rdu_lat,
5000.0 / (69.0 * cos(radians(35.88))) AS radius_degrees
)
SELECT code, name
FROM airports, search
WHERE ST_DWithin(
location,
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326),
radius_degrees
);
code | name
------+-------------------------
LAX | Los Angeles Intl
RDU | Raleigh-Durham Intl
ANC | Ted Stevens Anchorage
LHR | London Heathrow
EZE | Buenos Aires Ezeiza
LOS | Lagos Murtala Muhammed
ANC and LHR are correctly in the results, but now EZE and LOS appear even though both are more than 5,000 miles away. The latitude correction fixed one direction but widened the search enough to pull in airports that should be excluded.
The correction is also anchored to RDU’s latitude, not the latitude of each candidate airport, so it is only accurate for points at roughly the same latitude as the reference. There is no single degree threshold that produces correct results across a global dataset.
Casting to Geography
The right answer is to use geography and pass the threshold in meters. There is no conversion to manage and no latitude correction to apply.
WITH params AS (
SELECT
-78.7875 AS rdu_lon,
35.8776 AS rdu_lat,
1609.344 AS meters_per_mile
),
search AS (
SELECT rdu_lon, rdu_lat,
5000 * meters_per_mile AS radius_meters
FROM params
)
SELECT code, name
FROM airports, search
WHERE ST_DWithin(
location::geography,
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326)::geography,
radius_meters
);
code | name
------+------------------------
LAX | Los Angeles Intl
RDU | Raleigh-Durham Intl
ANC | Ted Stevens Anchorage
LHR | London Heathrow
(4 rows)
EZE and LOS, both more than 5,000 miles from RDU, are correctly excluded. The geography cast handles all spherical curvature and unit conversion internally.
A Better Path: Store as Geography
While you can cast between geometry and geography types, doing so in a WHERE clause prevents the query planner from using any index on the underlying column. An index is built on a specific expression — the raw column location — and when you write location::geography, the planner sees a function call wrapping the column rather than the column itself, so it falls back to a sequential scan. You can work around this with a functional index on the cast expression (CREATE INDEX ON airports USING GIST ((location::geography))), but if your use case consistently requires geography calculations, storing the column as geography from the start is simpler.
CREATE TABLE airports2 (code text, name text, location geography(Point, 4326));
INSERT INTO airports2 VALUES
('LAX', 'Los Angeles Intl', ST_GeogFromText('SRID=4326;POINT(-118.4081 33.9425)')),
('RDU', 'Raleigh-Durham Intl', ST_GeogFromText('SRID=4326;POINT(-78.7875 35.8776)')),
('ANC', 'Ted Stevens Anchorage', ST_GeogFromText('SRID=4326;POINT(-149.9961 61.1744)')),
('LHR', 'London Heathrow', ST_GeogFromText('SRID=4326;POINT(-0.4543 51.4775)')),
('EZE', 'Buenos Aires Ezeiza', ST_GeogFromText('SRID=4326;POINT(-58.5358 -34.8222)')),
('SVO', 'Moscow Sheremetyevo', ST_GeogFromText('SRID=4326;POINT(37.4146 55.9726)')),
('LOS', 'Lagos Murtala Muhammed',ST_GeogFromText('SRID=4326;POINT(3.3211 6.5774)')),
('SYD', 'Sydney Kingsford Smith',ST_GeogFromText('SRID=4326;POINT(151.1772 -33.9461)'));
And then select airports within 5000 miles of RDU:
WITH params AS (
SELECT
-78.7875 AS rdu_lon,
35.8776 AS rdu_lat,
1609.344 AS meters_per_mile
),
search AS (
SELECT rdu_lon, rdu_lat,
5000 * meters_per_mile AS radius_meters
FROM params
)
SELECT code, name
FROM airports2, search
WHERE ST_DWithin(
location,
ST_SetSRID(ST_MakePoint(rdu_lon, rdu_lat), 4326),
radius_meters
);
code | name
------+-----------------------
LAX | Los Angeles Intl
RDU | Raleigh-Durham Intl
ANC | Ted Stevens Anchorage
LHR | London Heathrow
When to Use Geography (and When Not To)
Use geography when your data spans large areas or crosses regions and your queries involve real-world distances: store finders, delivery routing, proximity search, and global asset tracking all belong here. Use geometry when your data is confined to a small, bounded region — local urban planning, parcel boundary analysis, and regional fleet operations within a single country are good fits, especially if you need the full PostGIS function library or are working with a specific projected coordinate system.