whatcrs
What CRS is this? Guess the coordinate reference system of mystery coordinates and get a ranked list of EPSG codes, each with a confidence and the reasons behind it.
For everyone who has received a CSV with X/Y columns and no projection, a shapefile
without a .prj, a GeoJSON that lands in the Gulf of Guinea, or a colleague's
"it's in UTM, I think".
$ whatcrs 440615 4474610 --hint Spain
1. EPSG:25830 ETRS89 / UTM zone 30N - confidence 23%
centre: 40.42000°N 3.70001°W (Spain)
WGS 84: -3.70001, 40.42000
- eastings near 500,000 and northings 4.47e+06 -> transverse Mercator / UTM pattern, northern hemisphere
- inverse projection lands at 40.4200°N, 3.7000°W in Spain
- all points inside the area of use (Europe between 6°W and 0°W: Faroe Islands offshore; ...)
- matches the hint 'Spain'
- commonly used CRS
2. EPSG:32630 WGS 84 / UTM zone 30N - confidence 11%
...
- Works offline with zero dependencies: a curated set of 876 widely used CRSs and pure-Python inverse projections that agree with PROJ to a few millimetres.
- With pyproj installed (
pip install whatcrs[pyproj]) it searches every non-deprecated EPSG projected CRS (about 5,300) and reports sample points in true WGS 84. - Understands hints such as
"Spain","probably in Germany","Texas","Europe","DE"or a bounding box. - Detects swapped axes (lat/lon vs lon/lat, northing/easting) and can fix them.
Install
pip install whatcrs # bundled CRS subset, pure Python
pip install "whatcrs[pyproj]" # full EPSG database through pyproj
Python 3.9+.
Command line
whatcrs 512345 4182345 # one point
whatcrs 512345 4182345 513000 4183100 # several points (also "x,y" tokens)
whatcrs points.csv --x lon --y lat --hint "Spain"
whatcrs parcels.geojson --json
whatcrs "POINT (2600000 1200000)" # WKT / EWKT
cat export.csv | whatcrs - --hint Texas --units ft
whatcrs 998114 194309 --bbox=-75,40,-73,41.5 # bbox: use "=" when it starts with "-"
Options: --hint, --bbox, --units m|ft|deg, --x/--y (CSV column name or 0-based
index), --delimiter, --format csv|tsv|geojson|wkt, --top N, --json,
--no-pyproj (bundled subset only), --no-swap. CSV columns named x/y, lon/lat,
easting/northing, rechtswert/hochwert... are found automatically, decimal commas are
handled, and a GeoJSON crs member is reported and compared with the guess. The exit code
is 0 when at least one candidate fits, 1 when none does and 2 on usage errors.
Python API
import whatcrs
cands = whatcrs.guess_crs([(440615, 4474610), (441000, 4475200)], hint="Spain")
best = cands[0]
best.epsg # 25830
best.code # 'EPSG:25830'
best.name # 'ETRS89 / UTM zone 30N'
best.confidence # 0.23 (softmax over all candidates)
best.axis_order # 'xy' or 'yx' (input was northing/latitude first)
best.country # 'Spain'
best.centroid # (-3.70, 40.42) lon/lat
best.sample_lonlat # first points, inverse-projected
best.reasons # list of plain-language explanations
best.to_dict() # JSON-ready
whatcrs.guess_crs(points, hint="Texas", units="ft") # State Plane feet
whatcrs.guess_crs(points, bbox=(5, 47, 15, 55), top=10) # (west, south, east, north)
whatcrs.guess_crs(points, use_pyproj=False) # bundled subset only
whatcrs.fix_swapped_axes([(52.52, 13.40), (52.50, 13.45)]) # -> [(13.40, 52.52), ...]
whatcrs.to_lonlat([(529755.8, 180719.2)], 27700) # pure-Python inverse
whatcrs.describe_coordinates(points) # what the magnitudes suggest
whatcrs.country_at(-3.70, 40.42) # 'Spain'
src = whatcrs.read_points("data.csv", x="E", y="N") # CSV / GeoJSON / WKT
src.points, src.declared_crs
fix_swapped_axes swaps the whole data set when the best guess says the input was (y, x)
and, for geographic data, also repairs single rows whose latitude and longitude were swapped.
How it works
- Candidates from magnitudes. Degree-sized values (|x| <= 180, |y| <= 90) are
geographic, in either axis order. Everything else is tested against every projected CRS
in the database: the median point is inverse-projected in both axis orders and kept only
if it lands within 2° of the CRS's EPSG area of use. That single step captures the
classic patterns - eastings around 500,000 m (UTM / TM false easting), northings of
4.2e6 m (about 38°N) or 10,000,000 m minus something (southern hemisphere), a leading
zone digit (Gauss-Krüger
4 468 746), State Plane feet, Web Mercator ranges, swapped columns. - Scoring. Each surviving (CRS, axis order) pair inverse-projects up to 40 sample
points and gets a log-score built from:
- the fraction of points inside the area of use, and how far the others miss it;
- the fraction on land (Natural Earth 1:50m countries, about 10 km coastal tolerance);
- the fraction inside the hinted region;
- specificity - a CRS with a small area of use explains the observed numbers better than a world-wide one (a likelihood term ~ 1 / area);
- a prior: commonly used CRSs (the bundled list), modern datums (ETRS89, NAD83,
GDA2020, SIRGAS 2000...) over legacy ones with identical numbers (ED50, NAD27...),
regional datums over the global WGS 84 variant, EPSG:4326 and EPSG:3857, the usual
axis order, and the
unitshint.
- Confidence is a softmax over the scores, so CRSs that are numerically indistinguishable (ETRS89 / UTM 30N and WGS 84 / UTM 30N agree to centimetres) share it.
All projection math is pure Python (whatcrs.projections): transverse Mercator with
Krüger's 6th-order series (as PROJ's default tmerc), UTM, Lambert conformal conic
(1SP/2SP), Albers, Lambert azimuthal equal area, polar and oblique (double) stereographic,
Swiss oblique Mercator, Mercator and Pseudo-Mercator, Cassini-Soldner, American polyconic,
Krovak, Hotine oblique Mercator (variants A/B), equidistant and equal-area cylindrical,
with linear units, axis orientation (+axis=wsu...) and prime meridians. Checked against
PROJ 9.8 for every non-deprecated EPSG projected CRS that uses these methods (5,287 CRSs):
5,285 agree to better than 2 mm inside their areas of use; two 19th-century Austrian
Cassini grids differ by about 250 m. Results are on the CRS's own datum; with pyproj the
reported sample points are transformed to WGS 84 including the datum shift.
Accuracy benchmark
benchmarks/benchmark.py draws 25 points in a ~30 x 30 km box around a place in each of
52 test cases covering 50 CRSs on every continent (UTM north/south, national grids,
Gauss-Krüger, State Plane feet, Lambert, Albers, LAEA, oblique stereographic, Krovak,
Hotine, polyconic, polar stereographic, Web Mercator, lon/lat and lat/lon), projects them
with pyproj and asks guess_crs for the top 3 - once without a hint and once with the
country (or US state) as hint. Exact means the expected EPSG code; equivalent means a
CRS that puts every point within 50 m of its true WGS 84 position (for example ETRS89 /
UTM 30N when WGS 84 / UTM 30N was used).
| Mode (52 cases) | Top-1 exact | Top-1 equivalent | Top-3 exact | Top-3 equivalent | Time per case |
|---|---|---|---|---|---|
| bundled subset, no hint | 31% | 33% | 38% | 40% | 0.30 s |
| bundled subset, with hint | 77% | 83% | 92% | 92% | 0.29 s |
| pyproj (all EPSG), no hint | 31% | 33% | 38% | 40% | 0.77 s |
| pyproj (all EPSG), with hint | 77% | 83% | 92% | 92% | 0.75 s |
(whatcrs 0.1.0, pyproj 3.8.0 / PROJ 9.8.1 / EPSG v12.029, Windows, Python 3.12; the first pyproj call also spends about 1-2 s reading the EPSG database.)
Reading the numbers:
- Give a hint. Without one, many inputs are genuinely ambiguous:
440000 4474000is a valid UTM coordinate in all 60 northern zones and lands on land in many of them. A country-level hint fixes most cases. - The remaining hinted misses are national multi-zone systems - Japan's 19 plane
rectangular zones, China's 3-degree Gauss-Krüger CMs, California's State Plane zones,
Australia's MGA zones - whose zones share false origins, so any zone puts the points
somewhere in the same country. A state/province hint (
"New South Wales","Texas") or abboxresolves them. - The bundled subset and the full EPSG database perform alike on these common CRSs; pyproj matters for the long tail of rarer EPSG codes.
Reproduce with python benchmarks/benchmark.py --verbose.
Bundled CRS subset (no pyproj)
876 CRSs from the EPSG dataset, including:
- geographic: WGS 84 (4326), ETRS89 (4258), NAD83 (4269), GDA94/GDA2020, SIRGAS 2000, CGCS2000, JGD2011, NZGD2000, NAD83(CSRS);
- world / polar: Web Mercator 3857, World Mercator 3395, 4087, UPS, Antarctic and Arctic polar stereographic (3031, 3413, 3995, 3976, 3996), EASE-Grid 2.0 (6931-6933);
- UTM: all WGS 84 zones north and south (32601-32660, 32701-32760), ETRS89 (25828-25837), NAD83 (26901-26923), NAD27 (26703-26722), ED50 (23028-23038), SIRGAS 2000 (31965-31985), SAD69, GDA94 / GDA2020 MGA (28348-28358, 7846-7859), JGD2011, NAD83(CSRS);
- Europe: British National Grid 27700, Irish Grid / ITM (29903, 2157), Lambert-93 2154 and CC42-CC50, NTF Lambert II étendu 27572, RD New 28992, Swiss LV95 / LV03 (2056, 21781), German Gauss-Krüger 31466-31469 and Soldner Berlin, Austrian GK and Lambert, Belgian Lambert 72/2008, Luxembourg, Portugal PT-TM06, Spain, Italy (Monte Mario, RDN2008), Greece 2100, Poland 2180 and 2176-2179, Czech/Slovak Krovak 5514, Hungary EOV 23700, Romania Stereo70 3844, Croatia, Slovenia, Baltic states, Sweden SWEREF99, Finland TM35FIN / GK, Iceland, Ukraine, Turkey TUREF/ED50 TM27-TM45, Pulkovo 1942 Gauss-Krüger, ETRS89 LAEA/LCC (3035, 3034);
- Asia: China CGCS2000 Gauss-Krüger (zones and CMs), Japan plane rectangular I-XIX, Korea, Taiwan TWD97, Hong Kong, Singapore SVY21, Malaysia RSO, Philippines PRS92, Vietnam VN-2000, Indonesia TM-3, India NSF LCC and Kalianpur zones, Israel;
- Africa: South Africa Lo zones, Egypt belts, Morocco, Nigeria, Kenya, Ghana (Gold Coast feet), Tunisia;
- Americas: NAD83 State Plane in metres and US feet (all states), Conus Albers 5070, US National Atlas, Alaska Albers, Canada Atlas Lambert, Statistics Canada, MTM, BC Albers, Quebec and Ontario Lambert, Mexico LCC, Costa Rica, Colombia (Origen Nacional and Bogotá zones), Argentina POSGAR 98/2007, Brazil polyconic / Mercator, Venezuela, Uruguay;
- Oceania: NZTM 2193, Australian Albers 3577, GA Lambert, NSW Lambert, VicGrid, Fiji.
whatcrs.builtin_records() lists them; tools/build_data.py regenerates the table.
Limitations
- Coordinates alone cannot separate CRSs that share projection parameters and differ only by datum (tens to hundreds of metres); the ranking prefers the modern regional datum.
- Bundled country outlines are simplified (about 3 km) Natural Earth 1:50m polygons; points within about 10 km of a coast count as land.
- Not handled: vertical CRSs, local engineering grids, degrees-minutes-seconds strings, coordinates scaled by an unknown factor.
Development
pip install -e ".[dev,pyproj]"
python -m pytest
python -m ruff check . && python -m ruff format --check src tests tools benchmarks
python -m mypy src
python benchmarks/benchmark.py
Data credits
- Country outlines and state/province boxes: Made with Natural Earth. Free vector and raster map data @ naturalearthdata.com (public domain), 1:50m Admin 0 countries and Admin 1 states/provinces.
- CRS definitions and areas of use: the EPSG Geodetic Parameter Dataset (IOGP), as distributed with PROJ.
License
MIT (c) Yusuf Eminoğlu. See LICENSE.
Metadata
Release files for whatcrs 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| whatcrs-0.1.0.tar.gz | 257.5 kB | With provenance | Details for whatcrs-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| whatcrs-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for whatcrs-0.1.0-py3-none-any.whl |
Total release size: 492.3 kB
Release files / whatcrs-0.1.0.tar.gz
| Download URL | whatcrs-0.1.0.tar.gz |
|---|---|
| Size | 257.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
266c53c8dcf4b3f14777e89301a915d24b1c9af49be429896985611104e18677
|
|
BLAKE2b-256 checksum How to use checksums |
d082ef2f518f39cda87c90abd89c3d59ed174c8ef132a95b474ae2663609fb33
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / whatcrs-0.1.0-py3-none-any.whl
| Download URL | whatcrs-0.1.0-py3-none-any.whl |
|---|---|
| Size | 234.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
78efac87e89d591266c8c88a9263782ac77667448fd8347b35f5debc5b57b544
|
|
BLAKE2b-256 checksum How to use checksums |
08871eab010b8f414ad52479ac4a1afb64a726c2980df2f746b3948ecffcf41b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for whatcrs 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for whatcrs-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.