Axis order

Two orders are involved, and this package keeps them separate.

Declared axis order is part of a CRS definition in the EPSG dataset. EPSG:4326 is declared latitude, longitude. EPSG:25832 is declared easting, northing. EPSG:2044 is declared northing, easting.

Value order is the order of the numbers you pass and get back. In this package it is always xy: longitude before latitude, easting before northing, then height – wherever the CRS has an easting and a northing to order. That is the order most data is stored in and most software expects. A CRS with no such pair (Krovak’s southing and westing, a geocentric X/Y/Z, a plant grid declaring north and west) keeps its declared order, exactly as PROJ’s always_xy keeps it; value_axis_order states the resulting order axis by axis.

CoordinateReferenceSystem reports both: axis_abbreviations is the declared order, and value_axis_abbreviations is the order values are passed in. Every TransformationResult has coordinate_order == "xy", and gives source_axes/target_axes in declared order beside it, so a reader can see where the two differ.

How it is achieved

Transformers are built with pyproj’s always_xy=True, which asks PROJ to normalise both ends of the pipeline to xy. The declared order is never used to reinterpret your values silently.

The gap in always_xy, and the workaround

always_xy normalises each end of a pipeline against that end’s declared horizontal axes. A vertical CRS has no horizontal axes, so there is nothing to normalise against at that end. The axisswap PROJ inserts for the operation’s own internal geographic CRS then survives, and the horizontal position is read latitude-first.

With stock PROJ 9.9.0:

from pyproj.transformer import TransformerGroup

# ETRS89 geographic 3D -> NN54 height, via a geoid grid
t = TransformerGroup("EPSG:4937", "EPSG:5776", always_xy=True).transformers[0]
t.transform(5.5, 58.5, 100.0)
# (58.5, 5.5, 57.087...)  -- the position comes back transposed

In the other direction, with the vertical CRS as the source, the same leftover swap makes PROJ read the position that comes with the height latitude-first. The geoid grid is then interpolated at the transposed position. If that position falls outside the grid, PROJ errors. If it falls inside, you get a plausible height for the wrong place, with no error.

This package inspects the pipeline’s entry step. If a vertical source’s pipeline reads the horizontal pair swapped, the package transposes the values before PROJ reads them, and writes the swap into the reported pipeline so that replaying it still reproduces the result. See Known issues and workarounds for the code location and when it can be removed.

Engineering CRSs

always_xy never normalises an engineering CRS, whatever its axes, and PROJ reads the origin a Similarity transformation states in a northing-first projected CRS in that CRS’s declared order even where the authority stated it easting-first – EPSG’s own EPSG:1035 does. Either leaves the values at an engineering end transposed with no error raised. This package corrects both: it adds the swap PROJ omits at a northing-first plant grid, and it settles the ordinate convention by placing the stated origin on the map under both readings, refusing the operation when neither or both land in the area of use. A grid with no east/north pair, such as EPSG:5800 (north, west), keeps its declared order, as the section above says. See Known issues and workarounds for the details, the code location and when each part can be retired.