Crossing The Antimeridian with PostGIS
September 04, 2026 · Jerome Gill
Normally when you want to do geospatial queries, you define a bounding box and ask your database to return geometries that have some relationship to the box.
The function for this is ST_MakeEnvelope
ST_MakeEnvelope(177, -20, 178, -16, 4326)
-- POLYGON((177 -20, 177 -16, 178 -16, 178 -20, 177 -20))
ST_MakeEnvelope takes 4 arguments (float xmin, float ymin, float xmax, float ymax) and returns a polygon with 4 points, once in each corner of a rectangle.
xmin and ymin are taken together to be the “bottom left” of the bounding box. xmax and ymax are taken to be the “top right”.
The Polygon format contains no indication about which point represents “bottom left” or “top right”. It simply stores an unordered collection of points. These can range from a simple rectangle to something incredibly complex with many, many points (think borders of a European country).
The function itself is agnostic as to what x and y are, but generally we consider them as longitude and latitude.
When doing so, we have 180 degrees of longitude y, ranging from -90 to 90 and 360 degrees of latitude, x. 0 longitude is taken to be the equator. 0 latitude is the meridian line, which runs through Greenwich, London. For… err… historical reasons.
gisgeography has a great diagram of the basics of what these numbers mean.
They each behave slightly differently, arithmetically, when we cross their extremes.
Across the poles
For longitude, if we take a value of, say 85 degrees north, and move 10 degrees further north, we take 5 degrees to cross the north pole, then find ourselves back where we started, a further 5 degrees from the north pole with a longitude of 85 once again.
But of course, we aren’t actually where we started. We crossed the north pole. And in that exact moment our latitude flipped 180 degrees.
And around the world
Circumnavigating the world at the equator means we have to implement wraparound logic somewhere.
We have two standard choices for where we do this, the meridian or, more commonly, the anti meridian.
If we take an x value of 0 to be the meridian line, to perform the wrap logic at the anti meridian we take a 360 degree scale from -180 to 180. Moving 10 degrees east from a position of 175 latitude, you cross the antimeridian and your latitude is now -175.
The alternative is to use a scale of 0 to 360 which gives us the wraparound logic at the meridian. Moving 10 degrees east at 355 degrees gives you a new latitude of 5.
Choosing where the world ends
You choose which scale to use based on how people are likely to use your application. You want to calculate as few bounding boxes that cross your chosen wraparound location as possible. We do this to avoid unnecessary calculation of an accepted workaround.
Remember that the Polygon consists of unordered points. We can calculate (and therefore draw) the shape of it by drawing a line from the smallest to largest values for [x,y].
If we draw a line from the smallest value of x and y to the largest value of x and smallest value of y we get a line along the bottom of our rectangle. Repeat this idea and we get the bounds.
Implementing this in code presents some problems because of a subtle difference in behaviour between ST_MakeEnvelope and the POLYGON data format it returns.
Say you want Fiji. It sits at about 177 degrees east on one side of that line and about 178 degrees west on the other, so your minimum longitude is 177 and your maximum is -178.
?bbox=177.0,-20.0,-178.0,-16.0
If you’re paying attention, you’ll notice the max is smaller than the min.
This results in a polygon as follows…
ST_MakeEnvelope(177, -20, -178, -16, 4326)
-- POLYGON((177 -20, 177 -16, -178 -16, -178 -20, 177 -20))
RFC 7946 §5.2 says a box crossing the antimeridian has a northeast longitude lower than its southwest one, and gives Fiji as the worked example.
To use it, we must interpret those points back into a shape. And if you interpret that Polygon as a rectangle whose bounds run between the smallest X value, to the largest, you get a rectangle with y bounds that you expect, but x bounds that run from -178 all the way to 177. That’s 355 degrees of the planet instead of the 5 you asked for.
So what do we do?
Since a single polygon can only ever be read smallest to largest, the answer is to use two of them, each with its top right corner larger than its bottom left:
ST_MakeEnvelope(177, -20, 180, -16, 4326) -- west of the line
ST_MakeEnvelope(-180, -20, -178, -16, 4326) -- east of it
Both are ordinary boxes. Together they cover the 5 degrees you meant.
This struck me as bizarrely hacky for what I presume must be a very common problem.
I’m sure this is the most basic thing in the world to anyone who has worked with 2d maps more than I have, but I found it interesting.
Here’s how I solved it with TypeOrm.
First we split our min and max longitude if it crosses the antimeridian at 180 into two min and maxs with extremes of 180 and -180
function splitAntimeridian({ minLon, maxLon }): [number, number][] {
return maxLon < minLon
? [
[minLon, 180],
[-180, maxLon],
]
: [[minLon, maxLon]];
}
Then OR the two ST_Intersects calls together, inside a bracketed group:
query.andWhere(
new Brackets((qb) => {
splitAntimeridian(bbox).forEach(([west, east], i) => {
qb.orWhere(
`ST_Intersects(geometry,
ST_MakeEnvelope(:west${i}, :minLat, :east${i}, :maxLat, 4326))`,
{ [`west${i}`]: west, [`east${i}`]: east, minLat, maxLat },
);
});
}),
);
Now ST_Intersects calculates whether geometries of interest are found within 2 bounding boxes instead of 1, on either side of the antimeridian.