Detect wrong or missing CRS on vector data, suggest the right EPSG code with a reason, and batch-reproject a whole folder.
A missing or wrong coordinate reference system is the single most common way a geospatial dataset goes silently wrong: the geometries load fine, plot on top of nothing, and every distance comes out nonsensical. crs-fixer inspects the raw coordinate extent of a dataset, tells you the CRS it was most likely recorded in (with a confidence score and a plain-language reason), and reprojects one file or an entire folder to the CRS you actually want.
Maintained by python-geospatial.com, a knowledge base for the modern Python geospatial stack.
- Guesses the CRS from coordinate bounds: geographic degrees (EPSG:4326),
Web Mercator (EPSG:3857), or a projected metric grid — and flags the classic
swapped
(lat, lon)axis-order mistake. - Suggests a UTM zone from an approximate longitude/latitude, using the correct 6-degree zoning maths.
- Assigns a CRS without transforming when the numbers are right but the metadata is missing — and refuses to clobber an existing CRS by accident.
- Batch-reprojects a folder, mirroring names into a new directory and never touching the source files, with a per-file status report.
Not published to PyPI — install straight from the repository:
pip install "git+https://github.com/python-geospatial/crs-fixer.git"or clone and install in editable mode:
git clone https://github.com/python-geospatial/crs-fixer.git
cd crs-fixer
pip install -e ".[dev]"crs-fixer detect parcels.geojsonBest guess EPSG:4326 WGS 84 (geographic, lon/lat degrees)
Confidence ████████████████░░░░ 80%
Rationale All coordinates fall within +-180 longitude and +-90 latitude,
so the data is almost certainly geographic degrees (WGS 84). If
the data is European, EPSG:4258 (ETRS89) is a common
near-identical alternative.
Alternatives EPSG:4258
Machine-readable output for scripts:
crs-fixer detect parcels.geojson --json{
"epsg": 4326,
"name": "WGS 84 (geographic, lon/lat degrees)",
"confidence": 0.8,
"rationale": "All coordinates fall within +-180 longitude and +-90 latitude ...",
"alternatives": [4258]
}crs-fixer utm 10.5 59.9lon=10.5, lat=59.9 -> EPSG:32632 (WGS 84 / UTM zone 32N)
crs-fixer reproject parcels.geojson parcels_utm.geojson --to EPSG:32632If the source has no CRS but you know what the coordinates represent, declare it
first with --assume:
crs-fixer reproject raw_grid.shp grid_utm.shp --to EPSG:32632 --assume EPSG:4326crs-fixer batch ./raw ./reprojected --to EPSG:25832 --assume EPSG:4326 --pattern "*.shp" Batch reproject -> EPSG:25832
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ File ┃ Status ┃ Source CRS ┃ Detail ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ parcels.shp │ ok │ EPSG:4326 │ Reprojected. │
│ floodplain.shp │ assigned │ EPSG:4326 │ Assigned assumed CRS, ... │
│ sensors.shp │ error │ - │ Reprojection or write ... │
└──────────────────┴──────────┴────────────┴───────────────────────────┘
2/3 reprojected, 1 need attention.
The command exits non-zero if any file failed or was skipped, so it drops cleanly into CI or a Makefile.
import geopandas as gpd
from crs_fixer import guess_crs, suggest_utm, assign_crs, reproject
parcels = gpd.read_file("parcels.geojson")
guess = guess_crs(parcels)
print(guess.epsg, guess.confidence, guess.rationale)
# Numbers are right, metadata missing -> label without transforming:
labelled = assign_crs(parcels, guess.epsg)
# Move the coordinates into a metric UTM zone for analysis:
utm_guess = suggest_utm(labelled, approx_lonlat=(10.5, 59.9))
parcels_utm = reproject(labelled, utm_guess.epsg)- Sound, documented heuristics — no magic, every guess carries a rationale.
- Correct UTM zone maths (
zone = int((lon + 180) / 6) + 1;326xxnorth,327xxsouth). - Axis-order gotcha detection for swapped
(lat, lon)data. - Batch mode never mutates source files and reports every file individually.
always_xy=Truediscipline and EPSG-vs-PROJ-string guidance baked into the docstrings.
guess_crs reads only the total bounds of the geometries and reasons about them:
coordinates inside [-180, 180] x [-90, 90] are geographic degrees; coordinates
inside the Web Mercator world extent (±20,037,508 m) are almost certainly
EPSG:3857; anything larger is a projected national/UTM grid whose zone can only
be pinned down with an external location hint. Assigning a CRS uses GeoPandas'
set_crs (metadata only), while reprojection uses to_crs (pyproj under the
hood). When you build a pyproj Transformer yourself, always pass
always_xy=True, and prefer authoritative EPSG codes over PROJ strings so datum
shifts are not silently dropped.
- Coordinate reference system transformations
- Coordinate Systems with PyProj
- GeoPandas DataFrames Explained
- Geospatial data ingestion & processing workflows
git clone https://github.com/python-geospatial/crs-fixer.git
cd crs-fixer
pip install -e ".[dev]"
ruff check .
pytestMIT — see LICENSE.