Metadata-Version: 2.4
Name: whatcrs
Version: 0.1.1
Summary: What CRS is this? Guess the coordinate reference system of mystery coordinates and rank EPSG codes with explanations.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/whatcrs_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/whatcrs_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/whatcrs_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/whatcrs_sdk/blob/main/CHANGELOG.md
Keywords: gis,crs,epsg,projection,coordinate-reference-system,srs,proj,utm,gauss-kruger,state-plane,web-mercator,geodesy,coordinates,geojson,csv
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pyproj
Requires-Dist: pyproj>=3.3.0; extra == "pyproj"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# 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".

```console
$ 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

```bash
pip install whatcrs            # bundled CRS subset, pure Python
pip install "whatcrs[pyproj]"  # full EPSG database through pyproj
```

Python 3.9+.

## Command line

```bash
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

```python
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

```bash
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](https://www.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](https://epsg.org)
  (IOGP), as distributed with [PROJ](https://proj.org).

## License

MIT (c) Yusuf Eminoğlu. See [LICENSE](LICENSE).
