Skip to main content

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

  1. 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.
  2. 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 units hint.
  3. 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 4474000 is 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 a bbox resolves 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)

Source distribution for whatcrs 0.1.0
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)

Table of built distributions (wheels) for whatcrs 0.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page