Short answer
There is no single universal order, and that is exactly why points end up in the ocean. Humans, GPS receivers, and the EPSG definition of WGS84 quote latitude, longitude. Most GIS software, GeoJSON, and graphics code use longitude, latitude, because they follow the mathematical X, Y convention where X (horizontal, east-west) is longitude and Y (vertical, north-south) is latitude. So the rule is: latitude is north-south, longitude is east-west, and the order they are written in depends on the format you are reading. When a feature at roughly (latitude 48, longitude 2) shows up near (latitude 2, longitude 48), you have an axis-order swap, not a projection problem.
The two conventions and why both exist
Latitude and longitude are unambiguous as quantities. Latitude runs from −90° (south pole) to +90° (north pole). Longitude runs from −180° to +180° around the equator, zero at Greenwich. The ambiguity is purely in ordering, and it comes from two communities that both got there first:
- The geodetic/navigation tradition writes latitude then longitude ("48.85, 2.35" for Paris). This is what your phone, a GPS unit, and the formal EPSG:4326 axis order use. EPSG:4326 explicitly defines axis 1 as latitude (north) and axis 2 as longitude (east).
- The computing/cartographic tradition treats a coordinate as a point on a Cartesian plane: X first, then Y. Since longitude maps to X and latitude to Y, this gives longitude, latitude. This is what GeoJSON, most JavaScript mapping libraries, GDAL/OGR's internal order, PostGIS constructors, and shapefiles use.
Both are "correct." The bug is moving data between them without translating.
What each common format expects
Knowing the convention per format eliminates most of the guesswork:
- GeoJSON (RFC 7946): strictly
[longitude, latitude], always, regardless of CRS (the spec fixes the CRS to WGS84 lon/lat). If your GeoJSON points are wrong, this is the first thing to check. - WKT / WKB geometry:
POINT(longitude latitude), i.e. X then Y. PostGISST_AsTextreturns lon, lat. - Shapefile: stores X, Y, so longitude, latitude.
- EPSG:4326 as a CRS authority: officially latitude, longitude. This clash with GeoJSON is the deepest source of confusion, and the reason GDAL added explicit axis-mapping controls.
- GPS / NMEA, and most human text: latitude, longitude.
- Leaflet:
L.latLng(lat, lon), latitude first (it deliberately matches GPS), whereas the GeoJSON it consumes is lon-first. Mixing the two in one codebase is a classic trap. - CSV: whatever the author chose, so you must inspect. A column literally named
latcontaining values above 90 tells you the columns are swapped.
How to read the symptom
The position of the wrong point usually diagnoses the cause:
- Mirrored across the diagonal (lat and lon values plausibly swapped, e.g. Paris appearing in the Indian Ocean near 2°N, 48°E): axis-order swap.
- Sitting at (0, 0) in the Gulf of Guinea ("Null Island"): a missing, null, or zeroed coordinate, often a failed parse or a default value, not an order problem.
- Shifted by a consistent small distance everywhere: a datum/transformation issue, not axis order.
- Off by a factor and clustered near the prime meridian: sometimes a degrees vs radians or a decimal-parsing error.
Worked fixes
ogr2ogr (GDAL). GDAL ≥ 3 honours the authority axis order by default, which can flip things unexpectedly. Force traditional GIS lon/lat order with the axis-mapping option:
ogr2ogr -f GPKG out.gpkg in.geojson \
-s_srs EPSG:4326 -t_srs EPSG:4326 \
--config OGR_GEOMETRY_ACCEPT_UNSAFE_AXIS_SWAP YES
More commonly you set the SRS with the explicit override EPSG:4326 plus the option to treat coordinates as lon/lat, or use -oo / -lco axis flags depending on driver. Always verify with ogrinfo -al -so out.gpkg and a visual check.
PostGIS. Construct points in X, Y order and set the SRID explicitly:
SELECT ST_SetSRID(ST_MakePoint(lon, lat), 4326) AS geom
FROM raw_points;
ST_MakePoint takes (X, Y) = (lon, lat). If you feed it (lat, lon), every point is swapped. To reproject for measurement, wrap with ST_Transform(geom, 32631).
QGIS "Add Delimited Text Layer". You explicitly choose which column is X and which is Y. Picking the latitude column as X is the usual user error; the preview map shows immediately if points land in the wrong hemisphere.
Code. Check the library contract. Turf.js and the GeoJSON spec want [lon, lat]; Leaflet wants (lat, lon). Write a single conversion helper and use it at every boundary rather than swapping ad hoc.
A practical workflow to prevent swaps
- Identify the source format and its convention before importing anything (use the table above).
- Inspect raw values. Any "latitude" outside −90..90 or "longitude" outside −180..180 proves a swap or unit error.
- Import with explicit axis assignment (name X and Y, or use the right constructor), never relying on defaults across a GDAL version boundary.
- Set, do not assign-then-hope, the SRID/CRS. Assigning EPSG:4326 to data that is actually lon/lat-ordered is fine; assigning it to swapped data just records the wrong order authoritatively.
- Visual control check against a known landmark, plus a coordinate spot-check of one feature with a known true position.
Common pitfalls and why they happen
- GeoJSON in, Leaflet out. GeoJSON is lon/lat; Leaflet's
latLngis lat/lon. Hand-passing arrays between them flips coordinates. Use Leaflet's GeoJSON layer, which handles the order for you. - Trusting GDAL defaults across versions. GDAL 2 and GDAL 3 differ on authority axis order; a script that worked silently can start swapping after an upgrade.
- Confusing axis order with reprojection. A swap mirrors points across the diagonal; reprojection moves them a consistent small amount. They have different fixes.
- Editing the CRS to "make it line up." Reassigning a CRS to nudge data is wrong if the underlying order is the problem; you must swap the axes, not relabel the system.
Validation and QA
After import, place one feature with a known location (a city, a survey marker) and confirm it lands there, in the right hemisphere. Confirm latitude values stay within −90..90 and longitude within −180..180. Run ogrinfo -al -so or PostGIS ST_AsText(geom) and read the first coordinate: it should be longitude for WKT/GeoJSON-derived data. Document the source convention, the import settings, and the working CRS in the project notes so the next analyst does not re-derive it from a misplaced point.
Bathyl perspective
Coordinate-order bugs are cheap to prevent and expensive to discover late, because the map often still "looks like a map," just of the wrong place. We pin the convention at every format boundary, set SRIDs explicitly, and verify one known point before trusting a dataset. The order rule is small; the discipline of checking it is what keeps a layer trustworthy.
Related reading
- On-the-Fly Reprojection in QGIS
- Reproject Vector Data Without Moving It Wrong
- Why Your GIS Layers Do Not Line Up
- GIS and spatial analysis