Your point is in the Gulf of Guinea, not Tokyo
A swapped latitude/longitude pair usually surfaces as a marker in the wrong hemisphere, a layer mirrored across the diagonal, or a distance that is wrong by a few hundred kilometres. It is a different failure from a point at exactly (0, 0), which is almost always a missing value rather than a reversed one.
The symptoms are recognizable before you read a single line of the stack trace:
- A marker that should be in Tokyo renders somewhere in the ocean, or does not render at all.
- Every feature in a layer is rotated into a mirrored, transposed version of the real geometry.
- A GeoJSON file that displays correctly in one viewer collapses into nonsense in another.
- A distance calculation returns a number that is wrong but not absurdly wrong.
That last one is the expensive case. MySQL's own engineering blog uses Trondheim and London as the demonstration: with the correct axis order, the distance is 1,464.25 km; with the axis order swapped, the same two rows produce 1,755.63 km (Axis Order in Spatial Reference Systems). Roughly 300 km of error, from data that passes every schema check you have.
It is worth separating two failure modes that get filed under the same bug report. A record sitting at exactly (0, 0) is almost always a missing coordinate rather than a swapped one. That location, where the prime meridian crosses the equator in open water near the Gulf of Guinea, is known in GIS as Null Island, a placeholder name for records that silently defaulted to zero; it entered wide circulation after being added to the Natural Earth dataset in 2011. There is even a real NOAA weather observation buoy there, Station 13010, nicknamed "Soul". A genuinely swapped pair behaves differently: it either produces a latitude outside the legal range of -90 to 90, or it produces a numerically valid point somewhere else entirely — sometimes far enough away to be obvious, sometimes not.
If it is any comfort, this is not a niche mistake. OpenStreetMap's own help forum puts it plainly: switched lon/lat is "easily the most common geo-programming error (and it goes both ways as there is no agreement on the correct order), and has happened essentially to everybody". The bug reports tend to land on whichever library the developer noticed it in — Leaflet issue #2495, for instance, reports a GeoJSON MultiPolygon rendering with latitude and longitude reversed in Leaflet 0.7.2, reproducible in Safari, Chrome, and Firefox, and fixable only by manually reversing the coordinates.
Two standards, both correct
The EPSG Dataset defines EPSG:4326 with latitude first. RFC 7946 defines GeoJSON with longitude first, normatively and exclusively. Both are in force, and a library obeying either one is behaving correctly. There is no "right" order in the abstract — only the order of whichever system you happen to be inside.
Here is the part that makes this problem durable rather than merely annoying: nobody is doing it wrong.
The EPSG Dataset — the authority that defines what EPSG:4326 means — declares the axis order of WGS 84 as latitude first. The official WKT2 (2019) definition spells it out: AXIS["geodetic latitude (Lat)", north, ORDER[1]], AXIS["geodetic longitude (Lon)", east, ORDER[2]]. MySQL's engineering blog states the same conclusion from the database side: "The EPSG Dataset standardizes on latitude-longitude order" for SRID 4326.
RFC 7946, the IETF specification for GeoJSON published in 2016, says the opposite and says it normatively. Section 3.1.1: "The first two elements are longitude and latitude, or easting and northing, precisely in that order and using decimal numbers". Section 4 goes further and removes the escape hatch — all GeoJSON coordinates must use a geographic CRS based on the WGS 84 datum with longitude/latitude in decimal degrees, and that is the only permitted CRS.
Both documents are in force. Both are being obeyed correctly, at the same time, by tools sitting next to each other in your dependency tree. "Which order is right" therefore has no answer. The only question with an answer is: which system am I inside right now — and that is a lookup question, not a memory question.
One clarification about the reasoning behind GeoJSON's choice. RFC 7946 states the rule but does not explain it; the normative text contains no rationale for putting longitude first. The explanation you usually see quoted comes from a personal blog post by Tom MacWright, a co-editor of the RFC — his own account, not spec language. He writes that "longitude, latitude matches the X, Y order of math," and observes the underlying split: "data formats usually use longitude, latitude order" while "applications have tended to use latitude, longitude order". He also notes how unproductive the argument has been: "Long hours have been wasted discussing which is better". That is useful context, but it is not normative text and should not be quoted as though it were.
The same post explains why GeoJSON stopped negotiating: the 2008 GeoJSON spec allowed alternate coordinate systems through a crs property, and "this capability was removed in the current GeoJSON standard".
WKT has two different answers depending on what you mean by WKT
WKT geometry text — POINT(x y) — puts longitude first, following OGC Simple Features. WKT2 for coordinate reference systems declares explicit AXIS elements, and for EPSG:4326 those put latitude first. The two are different grammars describing different layers, so quoting one against the other settles nothing.
"WKT" names two different things, and they give different answers.
WKT geometry text is the POINT(x y) / LINESTRING(...) family from OGC Simple Features. Coordinate order here is conventionally x, then y — longitude before latitude — regardless of what axis order the CRS authority defines for the underlying reference system. This is what you paste into ST_GeomFromText.
WKT for coordinate reference systems is a different grammar entirely, standardized in OGC 18-010r7. It does not describe a point; it describes a CRS, and it contains explicit AXIS declarations. Section 7.5.5 states that the axis order "identifies the order in which the coordinates of a point in a dataset are given and therefore is significant" — and, critically, that when a WKT description's axis order conflicts with a cited authority identifier such as an EPSG code, "the WKT values shall prevail". For EPSG:4326, those declarations put latitude first.
So: a POINT in WKT is lon-lat, and a CRS definition in WKT2 for the same datum says lat-lon. Both statements are true, and they are not about the same layer. When you see the two cited against each other in a thread, someone is comparing a geometry serialization to a coordinate system definition.
What each library actually does
GeoJSON, PostGIS, Turf, OpenLayers, Mapbox GL JS and KML take longitude first. Leaflet, the Google Maps JavaScript API, ISO 6709 and the EPSG:4326 CRS definition take latitude first. MySQL splits the difference: latitude-first for EPSG:4326 import and export, longitude-first in storage.
Every row below is a library correctly following one of the two conventions described above.
| Library / format | Coordinate order | Typical API or literal | Notes |
|---|---|---|---|
| GeoJSON (RFC 7946) | lon, lat | [-73.988214, 40.749128] |
Normative and exclusive; only a WGS 84-based geographic CRS is permitted |
| Leaflet | lat, lng | L.latLng(50.5, 30.5), L.marker([lat, lng]) |
Accepts [lat, lng] arrays or {lat, lng} objects — the opposite of GeoJSON |
| Mapbox GL JS | lng, lat | new LngLat(lng, lat), or a bare [lng, lat] array |
Docs state the order was chosen "to match the GeoJSON specification"; arrays are implicitly converted |
| Google Maps JS API | lat, lng | new google.maps.LatLng(-34, 151), {lat: -34, lng: 151} |
Latitude first in both constructor and object literal |
| OpenLayers | lon, lat | ol.proj.fromLonLat([lon, lat]) |
The FAQ says to "put the longitude first, and then the latitude"; the method name encodes the order |
| Turf.js | lon, lat | turf.point([-73.988214, 40.749128]) |
"Turf expects the data to be standard WGS84 longitude, latitude coordinates" |
| PostGIS | x = lon, y = lat | ST_MakePoint(lon, lat); ST_X() returns lon, ST_Y() returns lat |
Swapping the arguments is described in the ST_Point docs as "the single most common PostGIS bug" |
SQL Server geography |
lon, lat | STGeomFromText('LINESTRING(-122.360 47.656, -122.343 47.656)') |
Follows the OGC WKT geometry convention, X before Y |
| MySQL 8.0 | lat, lon for 4326 I/O; lon first internally | ST_GeomFromText() (axis-order parameter), ST_SwapXY() |
Follows OGC 08-038r5; storage is always lon-first, so axis order only affects import/export |
| Shapely / GeoPandas | x = lon, y = lat | Point(lon, lat), points_from_xy(df.Longitude, df.Latitude) |
GeoPandas documents points_from_xy as a wrapper for [Point(x, y) for x, y in zip(df.Longitude, df.Latitude)] |
| KML | lon, lat, alt | -122.0,37.4,0 — comma-separated, no spaces |
Altitude defaults to 0 (sea level) if omitted |
| WKT geometry (OGC SF) | x y = lon lat | POINT(-73.98 40.74) |
Independent of the CRS authority's declared axis order |
| WKT2 CRS definition, EPSG:4326 | lat, lon | AXIS["geodetic latitude (Lat)", north, ORDER[1]] |
This is a CRS definition, not a geometry |
| ISO 6709 | lat, lon | — | Point location standard; latitude first, unlike GeoJSON, WKT, PostGIS, and KML |
Two caveats on the table. The KML, ISO 6709, SQL Server, and MySQL rows rest on sources we would rate as good but not verified line-by-line against the primary normative text, so treat them as a starting point for a five-minute check rather than a citation of last resort. And the table describes defaults — most of these libraries have some override, and any of them can be wrapped by application code that reverses the order before you see it.
If what the table tells you is that your data has to move between coordinate systems rather than simply be relabelled, that is a job you can do without writing a one-off script: the coordinate converter covers the full EPSG database and accepts pasted batches, one coordinate per line, processing them in the browser.
Coordinate Converter
Convert coordinates between EPSG systems with batch support and smart detection.
Try it nowThe version boundaries that changed under you
Three upgrades flipped axis handling without any application code changing: WMS 1.1.1 to 1.3.0, PROJ 5 to PROJ 6, and GDAL 2.x to GDAL 3.0. If a map that rendered correctly last quarter is now mirrored, check the dependency bump before you start bisecting your own logic.
The nastiest variant of this bug is the one where nothing in your code changed. You upgraded a dependency, and a library that used to ignore the authority's axis order started honoring it.
| Boundary | Before | After |
|---|---|---|
| WMS 1.1.1 → 1.3.0 | BBOX for WGS84/EPSG:4326 in lon, lat | BBOX in lat, lon (see the note below this table) |
| PROJ 5 → PROJ 6 | Did not respect the axis ordering defined by the CRS authority | "PROJ respects the axis ordering as it was defined by the authority," per ISO 19111 |
| GDAL 2.x → GDAL 3.0 | OSRImportFromEPSG() assumed traditional GIS x,y order |
Takes the authority-defined axis order into account by default; the old behavior is restored with SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER) |
Daniel Morissette's account of the WMS change is that it came out of OGC's effort to have WMS 1.3.0 approved as an ISO standard, and that the ISO review process required the version to honor the axis order EPSG defines in its coordinate system database — one practitioner's blog post rather than an OGC statement, so read it as a plausible history. Taken with the previous section, that is the whole conflict in one sentence: standardization pressure pushed WMS toward the EPSG answer, while data-format practice settled on the other one.
Within a single toolchain, the defaults also do not always agree with each other. PROJ 6 honors authority axis order in the library, but the proj command-line utility still expects longitude/latitude input by default unless you pass -r to reverse it — worth confirming against your installed version's docs, since this is exactly the kind of detail that gets revised.
The ecosystem has been aware of this for a long time without resolving it. Turf.js issue #182, "Latlng vs LngLat," was opened on 2015-01-09 to discuss standardizing coordinate order, specifically for interoperability between Turf and Mapbox.js. No standards body appears to have issued a "use this order everywhere" recommendation since.
CRS84: the identifier that removes the ambiguity
urn:ogc:def:crs:OGC:1.3:CRS84 names WGS 84 with longitude first; the EPSG:4326 identifier names the same datum with latitude first. Same ellipsoid, same datum, different declared axis order, different identifier. Where a format lets you state a CRS, using CRS84 turns a convention into a contract.
There is a way to say "WGS 84, longitude first" without ambiguity, and it is not EPSG:4326.
urn:ogc:def:crs:OGC:1.3:CRS84 — equivalently http://www.opengis.net/def/crs/OGC/1.3/CRS84 — denotes WGS84 with axis order longitude, latitude; OGC's definition server returns exactly that, longitude-latitude, as the axis order for this identifier. http://www.opengis.net/def/crs/EPSG/0/4326 denotes the same datum with axis order latitude, longitude. Same ellipsoid, same datum, different declared axis order, different identifier. If you are designing an API or a service contract, using CRS84 where you mean lon-lat is the difference between a documented decision and a convention someone will violate.
GeoJSON took the other route to the same end. The 2008 spec's crs member, which let a document declare an alternate reference system, was removed entirely in RFC 7946. The removal was less disruptive than it sounds: the 2008 default, applied whenever no crs member was present, was already WGS84 geographic coordinates in decimal degrees — the same behavior RFC 7946 made mandatory and exclusive. GeoJSON did not negotiate the CRS at the document level at all — it fixed one and removed the knob.
How to tell if your data is swapped
Six checks, roughly in order of cost. Range-check the latitude field first — it is one comparison per row — then understand the band where that check cannot help you, look at the data on a map, pin a known-answer distance in CI, and record the order at every boundary instead of inferring it.
1. Range-check the value you believe is latitude. Valid latitude is -90 to 90; valid longitude is -180 to 180. Any "latitude" with magnitude above 90 is a longitude wearing the wrong label. This is the cheapest and highest-yield check you can write, and it costs one comparison per row.
2. Know exactly where that check goes blind. Working it through from those two ranges — this is a derivation, not something a source states — the latitude check only fires when the true longitude exceeds 90 in absolute value, and the longitude check never fires at all, because any genuine latitude is at most 90 and sits comfortably inside ±180. So for any point whose longitude falls between -90 and 90, and Europe, Africa and most of India sit inside that band, a swapped pair is numerically valid in both slots and passes silently. Range validation only catches the cases where the swapped value happens to land out of range.
// The check itself is trivial: a latitude cannot exceed 90.
const outOfRange = (lat) => Math.abs(lat) > 90;
// Tokyo (35.68, 139.69) arrives swapped as (139.69, 35.68):
outOfRange(139.69); // true — caught, because Tokyo is east of 90°E
// Paris (48.85, 2.35) arrives swapped as (2.35, 48.85):
outOfRange(2.35); // false — passes silently. The point now sits
// off the coast of Somalia, and nothing complains.
Inside that blind band, looking at the data beats validating it. Paste the file into an editor with a live map preview — the GeoJSON editor renders features as you type, in the browser — and a swapped dataset usually announces itself by drawing your city in the wrong country.
3. Do not validate a field against the other field's bound. The anti-pattern is checking a variable against the wrong limit — validating latitude with longitude's range or the reverse. Write at least one test case with an out-of-range value in each position separately.
// Anti-pattern: one bound reused for both fields.
const loose = (lat, lon) => Math.abs(lat) <= 180 && Math.abs(lon) <= 180;
loose(150, 20); // true — 150 is not a valid latitude, but ±180 lets it through
// Each field against its own bound.
const strict = (lat, lon) => Math.abs(lat) <= 90 && Math.abs(lon) <= 180;
strict(150, 20); // false — caught
4. Pin a known-answer fixture. Pick two locations with a distance you can verify and assert on it in CI. MySQL's blog gives a ready-made pair: Trondheim to London is 1,464.25 km with the correct axis order and 1,755.63 km with it swapped.
Do not hard-code those two numbers. They are the output of one specific distance implementation, and distance functions disagree — a spherical formula such as PostGIS's ST_DistanceSphere and an ellipsoidal one such as ST_Distance on geography will not return the same value for the same pair of points. Run the pair through the function your own stack uses and pin that result. What transfers is not the figure but the gap: an axis-order error moves this distance by roughly 300 km, which is orders of magnitude larger than the disagreement between distance models, so the assertion stays meaningful whichever function you pin it to. A single assertion like that catches an axis-order regression introduced by a dependency upgrade, which a range check on well-behaved European data never will.
-- Run once against your own stack, then pin the numbers it returns.
SELECT
ST_Distance(
ST_MakePoint(10.3951, 63.4305)::geography, -- Trondheim, lon first
ST_MakePoint(-0.1278, 51.5074)::geography -- London, lon first
) / 1000 AS correct_km,
ST_Distance(
ST_MakePoint(63.4305, 10.3951)::geography, -- the same rows, swapped
ST_MakePoint(51.5074, -0.1278)::geography
) / 1000 AS swapped_km;
5. Record the order at every boundary instead of inferring it. GeoPandas states the constraint directly: there is no way for the library to know at load time whether an external dataset's coordinates are in lon,lat or lat,lon order — the developer has to verify it. This is true of every library in the table above, not just GeoPandas. The order is metadata that lives outside the numbers, so it has to travel with them: in the column name, in the API contract, in a CRS identifier such as CRS84 where the format supports one.
6. Treat (0, 0) as a separate diagnosis. Points at Null Island are usually absent coordinates that defaulted to zero rather than swapped ones. Filter and count them independently, or you will spend an afternoon debugging an axis-order problem that is actually a null-handling problem.
FAQ
Is EPSG:4326 latitude-longitude or longitude-latitude?
Latitude first. The EPSG Dataset declares WGS 84's axis order as latitude, then longitude, and the WKT2 definition makes it explicit with ORDER[1] on latitude. Libraries that emit [lon, lat] for EPSG:4326 are following the GeoJSON or OGC Simple Features convention instead, not the EPSG authority.
Why does GeoJSON use longitude first?
RFC 7946 mandates it in Section 3.1.1 but gives no rationale. The explanation usually quoted — that longitude, latitude matches the x, y order of mathematics — comes from a personal blog post by one of the RFC's co-editors, not from the specification. It is useful context, not normative text.
How do I fix a GeoJSON file with swapped coordinates?
Confirm the swap first by rendering the file on a map rather than trusting a range check, which cannot detect swaps inside ±90° longitude. Once confirmed, reverse each coordinate pair. If the underlying problem is a CRS mismatch rather than an axis-order mismatch, reversing the pairs will not help — the data needs an actual transformation between coordinate systems, which is a different job with its own set of traps.
What is Null Island, and does a point at (0, 0) mean my data is swapped?
Null Island is the nickname for the point where the prime meridian meets the equator, in open water near the Gulf of Guinea. Records landing there are almost always missing coordinates that defaulted to zero, not swapped ones. Diagnose and count them separately.
Does CRS84 mean the same thing as EPSG:4326?
Same datum and ellipsoid, different declared axis order. urn:ogc:def:crs:OGC:1.3:CRS84 means WGS 84 with longitude first; EPSG:4326 means WGS 84 with latitude first. If you are writing an API contract and you mean lon-lat, naming CRS84 removes the ambiguity that EPSG:4326 leaves open.
Which libraries put latitude first?
Leaflet, the Google Maps JavaScript API, ISO 6709, and the EPSG:4326 CRS definition itself. Longitude first: GeoJSON, PostGIS, Turf.js, OpenLayers, Mapbox GL JS, KML, and WKT geometry text. MySQL is latitude-first for EPSG:4326 import and export but longitude-first in storage.