whatcrs 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. whatcrs-0.1.0/CHANGELOG.md +35 -0
  2. whatcrs-0.1.0/CITATION.cff +19 -0
  3. whatcrs-0.1.0/LICENSE +21 -0
  4. whatcrs-0.1.0/MANIFEST.in +11 -0
  5. whatcrs-0.1.0/PKG-INFO +261 -0
  6. whatcrs-0.1.0/README.md +225 -0
  7. whatcrs-0.1.0/benchmarks/benchmark.py +192 -0
  8. whatcrs-0.1.0/pyproject.toml +119 -0
  9. whatcrs-0.1.0/setup.cfg +4 -0
  10. whatcrs-0.1.0/src/whatcrs/__init__.py +41 -0
  11. whatcrs-0.1.0/src/whatcrs/__main__.py +3 -0
  12. whatcrs-0.1.0/src/whatcrs/cli.py +192 -0
  13. whatcrs-0.1.0/src/whatcrs/core.py +686 -0
  14. whatcrs-0.1.0/src/whatcrs/crs_db.py +289 -0
  15. whatcrs-0.1.0/src/whatcrs/data/NOTICE.md +10 -0
  16. whatcrs-0.1.0/src/whatcrs/data/countries.json.gz +0 -0
  17. whatcrs-0.1.0/src/whatcrs/data/crs_builtin.json.gz +0 -0
  18. whatcrs-0.1.0/src/whatcrs/projections.py +1056 -0
  19. whatcrs-0.1.0/src/whatcrs/py.typed +0 -0
  20. whatcrs-0.1.0/src/whatcrs/readers.py +297 -0
  21. whatcrs-0.1.0/src/whatcrs/regions.py +435 -0
  22. whatcrs-0.1.0/src/whatcrs.egg-info/PKG-INFO +261 -0
  23. whatcrs-0.1.0/src/whatcrs.egg-info/SOURCES.txt +32 -0
  24. whatcrs-0.1.0/src/whatcrs.egg-info/dependency_links.txt +1 -0
  25. whatcrs-0.1.0/src/whatcrs.egg-info/entry_points.txt +2 -0
  26. whatcrs-0.1.0/src/whatcrs.egg-info/requires.txt +9 -0
  27. whatcrs-0.1.0/src/whatcrs.egg-info/top_level.txt +1 -0
  28. whatcrs-0.1.0/tests/test_core.py +220 -0
  29. whatcrs-0.1.0/tests/test_projections.py +166 -0
  30. whatcrs-0.1.0/tests/test_pyproj_mode.py +52 -0
  31. whatcrs-0.1.0/tests/test_readers_cli.py +190 -0
  32. whatcrs-0.1.0/tests/test_regions.py +111 -0
  33. whatcrs-0.1.0/tests/test_vs_pyproj.py +111 -0
  34. whatcrs-0.1.0/tools/build_data.py +360 -0
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-08
8
+ ### Added
9
+ - `guess_crs(points, hint=None, *, bbox=None, units=None, top=5, use_pyproj=None,
10
+ allow_swapped=True)` ranks candidate EPSG codes for mystery coordinates. Each `Candidate`
11
+ carries the EPSG code, name, confidence, axis order, area of use, inverse-projected
12
+ sample points, the country the points land in and plain-language reasons.
13
+ - Candidate generation from coordinate magnitudes (degrees vs projected values, UTM / TM
14
+ false eastings, zone-prefixed Gauss-Krüger eastings, Web Mercator ranges, swapped axes)
15
+ and scoring by area of use, land vs sea, hinted region, CRS specificity and a prior that
16
+ favours widely used, modern CRSs.
17
+ - Pure-Python inverse projections (`whatcrs.projections`): transverse Mercator (Krüger
18
+ 6th-order series), UTM, Lambert conformal conic (1SP/2SP), Albers, Lambert azimuthal equal
19
+ area, polar and oblique stereographic, Swiss oblique Mercator, Mercator / Pseudo-Mercator,
20
+ Cassini-Soldner, American polyconic, Krovak, Hotine oblique Mercator, equidistant and
21
+ equal-area cylindrical. Checked against PROJ: under 2 mm for 5,285 of 5,287 EPSG projected CRSs using these methods.
22
+ - Bundled curated subset of 876 CRSs (WGS 84 and regional geographic CRSs, Web Mercator, all
23
+ WGS 84 UTM zones, ETRS89 / NAD83 / NAD27 / ED50 / SIRGAS 2000 / GDA94 / GDA2020 / JGD2011
24
+ UTM, NAD83 State Plane in metres and feet, and national grids worldwide). With `pyproj`
25
+ installed every non-deprecated EPSG projected CRS (about 5,300) is searched.
26
+ - Region hints: country names in several languages, ISO codes, continents, UN subregions,
27
+ groupings, US states / Canadian provinces / Australian states and other first-level
28
+ subdivisions, free text ("probably in Germany") and bounding boxes.
29
+ - `fix_swapped_axes` (whole data set and single swapped rows), `to_lonlat`,
30
+ `describe_coordinates`, `country_at`, `parse_hint`.
31
+ - Readers for CSV/TSV (column sniffing, decimal commas), GeoJSON (reports a declared `crs`)
32
+ and WKT/EWKT.
33
+ - `whatcrs` command line tool with text and `--json` output.
34
+ - Accuracy benchmark in `benchmarks/benchmark.py`.
35
+ - Natural Earth 1:50m country outlines (public domain) for land and country tests.
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "yusufeminoglu@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "whatcrs"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-08"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/whatcrs_sdk"
13
+ url: "https://pypi.org/project/whatcrs/"
14
+ abstract: "What CRS is this? Guess the coordinate reference system of mystery coordinates worldwide and rank candidate EPSG codes with explanations, using pure-Python inverse projections, EPSG areas of use and Natural Earth country outlines."
15
+ keywords:
16
+ - GIS
17
+ - coordinate reference system
18
+ - EPSG
19
+ - map projection
whatcrs-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/whatcrs/py.typed
7
+ recursive-include src/whatcrs *.py
8
+ recursive-include src/whatcrs/data *.json.gz *.md
9
+ recursive-include tests *.py
10
+ recursive-include tools *.py
11
+ recursive-include benchmarks *.py
whatcrs-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: whatcrs
3
+ Version: 0.1.0
4
+ Summary: What CRS is this? Guess the coordinate reference system of mystery coordinates and rank EPSG codes with explanations.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/whatcrs_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/whatcrs_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/whatcrs_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/whatcrs_sdk/blob/main/CHANGELOG.md
11
+ Keywords: gis,crs,epsg,projection,coordinate-reference-system,srs,proj,utm,gauss-kruger,state-plane,web-mercator,geodesy,coordinates,geojson,csv
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Topic :: Scientific/Engineering :: GIS
21
+ Classifier: Intended Audience :: Science/Research
22
+ Classifier: Intended Audience :: Developers
23
+ Classifier: Environment :: Console
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Provides-Extra: pyproj
29
+ Requires-Dist: pyproj>=3.3.0; extra == "pyproj"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # whatcrs
38
+
39
+ **What CRS is this?** Guess the coordinate reference system of mystery coordinates and get a
40
+ ranked list of EPSG codes, each with a confidence and the reasons behind it.
41
+
42
+ For everyone who has received a CSV with `X`/`Y` columns and no projection, a shapefile
43
+ without a `.prj`, a GeoJSON that lands in the Gulf of Guinea, or a colleague's
44
+ "it's in UTM, I think".
45
+
46
+ ```console
47
+ $ whatcrs 440615 4474610 --hint Spain
48
+ 1. EPSG:25830 ETRS89 / UTM zone 30N - confidence 23%
49
+ centre: 40.42000°N 3.70001°W (Spain)
50
+ WGS 84: -3.70001, 40.42000
51
+ - eastings near 500,000 and northings 4.47e+06 -> transverse Mercator / UTM pattern, northern hemisphere
52
+ - inverse projection lands at 40.4200°N, 3.7000°W in Spain
53
+ - all points inside the area of use (Europe between 6°W and 0°W: Faroe Islands offshore; ...)
54
+ - matches the hint 'Spain'
55
+ - commonly used CRS
56
+ 2. EPSG:32630 WGS 84 / UTM zone 30N - confidence 11%
57
+ ...
58
+ ```
59
+
60
+ * Works **offline with zero dependencies**: a curated set of 876 widely used CRSs and
61
+ pure-Python inverse projections that agree with PROJ to a few millimetres.
62
+ * With **pyproj** installed (`pip install whatcrs[pyproj]`) it searches every
63
+ non-deprecated EPSG projected CRS (about 5,300) and reports sample points in true WGS 84.
64
+ * Understands hints such as `"Spain"`, `"probably in Germany"`, `"Texas"`, `"Europe"`,
65
+ `"DE"` or a bounding box.
66
+ * Detects swapped axes (lat/lon vs lon/lat, northing/easting) and can fix them.
67
+
68
+ ## Install
69
+
70
+ ```bash
71
+ pip install whatcrs # bundled CRS subset, pure Python
72
+ pip install "whatcrs[pyproj]" # full EPSG database through pyproj
73
+ ```
74
+
75
+ Python 3.9+.
76
+
77
+ ## Command line
78
+
79
+ ```bash
80
+ whatcrs 512345 4182345 # one point
81
+ whatcrs 512345 4182345 513000 4183100 # several points (also "x,y" tokens)
82
+ whatcrs points.csv --x lon --y lat --hint "Spain"
83
+ whatcrs parcels.geojson --json
84
+ whatcrs "POINT (2600000 1200000)" # WKT / EWKT
85
+ cat export.csv | whatcrs - --hint Texas --units ft
86
+ whatcrs 998114 194309 --bbox=-75,40,-73,41.5 # bbox: use "=" when it starts with "-"
87
+ ```
88
+
89
+ Options: `--hint`, `--bbox`, `--units m|ft|deg`, `--x/--y` (CSV column name or 0-based
90
+ index), `--delimiter`, `--format csv|tsv|geojson|wkt`, `--top N`, `--json`,
91
+ `--no-pyproj` (bundled subset only), `--no-swap`. CSV columns named `x/y`, `lon/lat`,
92
+ `easting/northing`, `rechtswert/hochwert`... are found automatically, decimal commas are
93
+ handled, and a GeoJSON `crs` member is reported and compared with the guess. The exit code
94
+ is 0 when at least one candidate fits, 1 when none does and 2 on usage errors.
95
+
96
+ ## Python API
97
+
98
+ ```python
99
+ import whatcrs
100
+
101
+ cands = whatcrs.guess_crs([(440615, 4474610), (441000, 4475200)], hint="Spain")
102
+ best = cands[0]
103
+ best.epsg # 25830
104
+ best.code # 'EPSG:25830'
105
+ best.name # 'ETRS89 / UTM zone 30N'
106
+ best.confidence # 0.23 (softmax over all candidates)
107
+ best.axis_order # 'xy' or 'yx' (input was northing/latitude first)
108
+ best.country # 'Spain'
109
+ best.centroid # (-3.70, 40.42) lon/lat
110
+ best.sample_lonlat # first points, inverse-projected
111
+ best.reasons # list of plain-language explanations
112
+ best.to_dict() # JSON-ready
113
+
114
+ whatcrs.guess_crs(points, hint="Texas", units="ft") # State Plane feet
115
+ whatcrs.guess_crs(points, bbox=(5, 47, 15, 55), top=10) # (west, south, east, north)
116
+ whatcrs.guess_crs(points, use_pyproj=False) # bundled subset only
117
+
118
+ whatcrs.fix_swapped_axes([(52.52, 13.40), (52.50, 13.45)]) # -> [(13.40, 52.52), ...]
119
+ whatcrs.to_lonlat([(529755.8, 180719.2)], 27700) # pure-Python inverse
120
+ whatcrs.describe_coordinates(points) # what the magnitudes suggest
121
+ whatcrs.country_at(-3.70, 40.42) # 'Spain'
122
+ src = whatcrs.read_points("data.csv", x="E", y="N") # CSV / GeoJSON / WKT
123
+ src.points, src.declared_crs
124
+ ```
125
+
126
+ `fix_swapped_axes` swaps the whole data set when the best guess says the input was (y, x)
127
+ and, for geographic data, also repairs single rows whose latitude and longitude were swapped.
128
+
129
+ ## How it works
130
+
131
+ 1. **Candidates from magnitudes.** Degree-sized values (|x| <= 180, |y| <= 90) are
132
+ geographic, in either axis order. Everything else is tested against every projected CRS
133
+ in the database: the median point is inverse-projected in both axis orders and kept only
134
+ if it lands within 2° of the CRS's EPSG area of use. That single step captures the
135
+ classic patterns - eastings around 500,000 m (UTM / TM false easting), northings of
136
+ 4.2e6 m (about 38°N) or 10,000,000 m minus something (southern hemisphere), a leading
137
+ zone digit (Gauss-Krüger `4 468 746`), State Plane feet, Web Mercator ranges, swapped
138
+ columns.
139
+ 2. **Scoring.** Each surviving (CRS, axis order) pair inverse-projects up to 40 sample
140
+ points and gets a log-score built from:
141
+ * the fraction of points inside the area of use, and how far the others miss it;
142
+ * the fraction on land (Natural Earth 1:50m countries, about 10 km coastal tolerance);
143
+ * the fraction inside the hinted region;
144
+ * specificity - a CRS with a small area of use explains the observed numbers better
145
+ than a world-wide one (a likelihood term ~ 1 / area);
146
+ * a prior: commonly used CRSs (the bundled list), modern datums (ETRS89, NAD83,
147
+ GDA2020, SIRGAS 2000...) over legacy ones with identical numbers (ED50, NAD27...),
148
+ regional datums over the global WGS 84 variant, EPSG:4326 and EPSG:3857, the usual
149
+ axis order, and the `units` hint.
150
+ 3. **Confidence** is a softmax over the scores, so CRSs that are numerically
151
+ indistinguishable (ETRS89 / UTM 30N and WGS 84 / UTM 30N agree to centimetres) share it.
152
+
153
+ All projection math is pure Python (`whatcrs.projections`): transverse Mercator with
154
+ Krüger's 6th-order series (as PROJ's default `tmerc`), UTM, Lambert conformal conic
155
+ (1SP/2SP), Albers, Lambert azimuthal equal area, polar and oblique (double) stereographic,
156
+ Swiss oblique Mercator, Mercator and Pseudo-Mercator, Cassini-Soldner, American polyconic,
157
+ Krovak, Hotine oblique Mercator (variants A/B), equidistant and equal-area cylindrical,
158
+ with linear units, axis orientation (`+axis=wsu`...) and prime meridians. Checked against
159
+ PROJ 9.8 for every non-deprecated EPSG projected CRS that uses these methods (5,287 CRSs):
160
+ 5,285 agree to better than 2 mm inside their areas of use; two 19th-century Austrian
161
+ Cassini grids differ by about 250 m. Results are on the CRS's own datum; with pyproj the
162
+ reported sample points are transformed to WGS 84 including the datum shift.
163
+
164
+ ## Accuracy benchmark
165
+
166
+ `benchmarks/benchmark.py` draws 25 points in a ~30 x 30 km box around a place in each of
167
+ 52 test cases covering 50 CRSs on every continent (UTM north/south, national grids,
168
+ Gauss-Krüger, State Plane feet, Lambert, Albers, LAEA, oblique stereographic, Krovak,
169
+ Hotine, polyconic, polar stereographic, Web Mercator, lon/lat and lat/lon), projects them
170
+ with pyproj and asks `guess_crs` for the top 3 - once without a hint and once with the
171
+ country (or US state) as hint. *Exact* means the expected EPSG code; *equivalent* means a
172
+ CRS that puts every point within 50 m of its true WGS 84 position (for example ETRS89 /
173
+ UTM 30N when WGS 84 / UTM 30N was used).
174
+
175
+ | Mode (52 cases) | Top-1 exact | Top-1 equivalent | Top-3 exact | Top-3 equivalent | Time per case |
176
+ |---|---:|---:|---:|---:|---:|
177
+ | bundled subset, no hint | 31% | 33% | 38% | 40% | 0.30 s |
178
+ | bundled subset, with hint | **77%** | **83%** | **92%** | **92%** | 0.29 s |
179
+ | pyproj (all EPSG), no hint | 31% | 33% | 38% | 40% | 0.77 s |
180
+ | pyproj (all EPSG), with hint | **77%** | **83%** | **92%** | **92%** | 0.75 s |
181
+
182
+ (whatcrs 0.1.0, pyproj 3.8.0 / PROJ 9.8.1 / EPSG v12.029, Windows, Python 3.12; the first
183
+ pyproj call also spends about 1-2 s reading the EPSG database.)
184
+
185
+ Reading the numbers:
186
+
187
+ * **Give a hint.** Without one, many inputs are genuinely ambiguous: `440000 4474000` is a
188
+ valid UTM coordinate in all 60 northern zones and lands on land in many of them. A
189
+ country-level hint fixes most cases.
190
+ * The remaining hinted misses are national multi-zone systems - Japan's 19 plane
191
+ rectangular zones, China's 3-degree Gauss-Krüger CMs, California's State Plane zones,
192
+ Australia's MGA zones - whose zones share false origins, so any zone puts the points
193
+ somewhere in the same country. A state/province hint (`"New South Wales"`, `"Texas"`) or
194
+ a `bbox` resolves them.
195
+ * The bundled subset and the full EPSG database perform alike on these common CRSs; pyproj
196
+ matters for the long tail of rarer EPSG codes.
197
+
198
+ Reproduce with `python benchmarks/benchmark.py --verbose`.
199
+
200
+ ## Bundled CRS subset (no pyproj)
201
+
202
+ 876 CRSs from the EPSG dataset, including:
203
+
204
+ * geographic: WGS 84 (4326), ETRS89 (4258), NAD83 (4269), GDA94/GDA2020, SIRGAS 2000,
205
+ CGCS2000, JGD2011, NZGD2000, NAD83(CSRS);
206
+ * world / polar: Web Mercator 3857, World Mercator 3395, 4087, UPS, Antarctic and Arctic
207
+ polar stereographic (3031, 3413, 3995, 3976, 3996), EASE-Grid 2.0 (6931-6933);
208
+ * UTM: all WGS 84 zones north and south (32601-32660, 32701-32760), ETRS89 (25828-25837),
209
+ NAD83 (26901-26923), NAD27 (26703-26722), ED50 (23028-23038), SIRGAS 2000 (31965-31985),
210
+ SAD69, GDA94 / GDA2020 MGA (28348-28358, 7846-7859), JGD2011, NAD83(CSRS);
211
+ * Europe: British National Grid 27700, Irish Grid / ITM (29903, 2157), Lambert-93 2154 and
212
+ CC42-CC50, NTF Lambert II étendu 27572, RD New 28992, Swiss LV95 / LV03 (2056, 21781),
213
+ German Gauss-Krüger 31466-31469 and Soldner Berlin, Austrian GK and Lambert, Belgian
214
+ Lambert 72/2008, Luxembourg, Portugal PT-TM06, Spain, Italy (Monte Mario, RDN2008),
215
+ Greece 2100, Poland 2180 and 2176-2179, Czech/Slovak Krovak 5514, Hungary EOV 23700,
216
+ Romania Stereo70 3844, Croatia, Slovenia, Baltic states, Sweden SWEREF99, Finland
217
+ TM35FIN / GK, Iceland, Ukraine, Turkey TUREF/ED50 TM27-TM45, Pulkovo 1942 Gauss-Krüger,
218
+ ETRS89 LAEA/LCC (3035, 3034);
219
+ * Asia: China CGCS2000 Gauss-Krüger (zones and CMs), Japan plane rectangular I-XIX,
220
+ Korea, Taiwan TWD97, Hong Kong, Singapore SVY21, Malaysia RSO, Philippines PRS92,
221
+ Vietnam VN-2000, Indonesia TM-3, India NSF LCC and Kalianpur zones, Israel;
222
+ * Africa: South Africa Lo zones, Egypt belts, Morocco, Nigeria, Kenya, Ghana (Gold Coast
223
+ feet), Tunisia;
224
+ * Americas: NAD83 State Plane in metres and US feet (all states), Conus Albers 5070, US
225
+ National Atlas, Alaska Albers, Canada Atlas Lambert, Statistics Canada, MTM, BC Albers,
226
+ Quebec and Ontario Lambert, Mexico LCC, Costa Rica, Colombia (Origen Nacional and Bogotá
227
+ zones), Argentina POSGAR 98/2007, Brazil polyconic / Mercator, Venezuela, Uruguay;
228
+ * Oceania: NZTM 2193, Australian Albers 3577, GA Lambert, NSW Lambert, VicGrid, Fiji.
229
+
230
+ `whatcrs.builtin_records()` lists them; `tools/build_data.py` regenerates the table.
231
+
232
+ ## Limitations
233
+
234
+ * Coordinates alone cannot separate CRSs that share projection parameters and differ only
235
+ by datum (tens to hundreds of metres); the ranking prefers the modern regional datum.
236
+ * Bundled country outlines are simplified (about 3 km) Natural Earth 1:50m polygons; points
237
+ within about 10 km of a coast count as land.
238
+ * Not handled: vertical CRSs, local engineering grids, degrees-minutes-seconds strings,
239
+ coordinates scaled by an unknown factor.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ pip install -e ".[dev,pyproj]"
245
+ python -m pytest
246
+ python -m ruff check . && python -m ruff format --check src tests tools benchmarks
247
+ python -m mypy src
248
+ python benchmarks/benchmark.py
249
+ ```
250
+
251
+ ## Data credits
252
+
253
+ * Country outlines and state/province boxes: **Made with Natural Earth.** Free vector and
254
+ raster map data @ [naturalearthdata.com](https://www.naturalearthdata.com/) (public
255
+ domain), 1:50m Admin 0 countries and Admin 1 states/provinces.
256
+ * CRS definitions and areas of use: the [EPSG Geodetic Parameter Dataset](https://epsg.org)
257
+ (IOGP), as distributed with [PROJ](https://proj.org).
258
+
259
+ ## License
260
+
261
+ MIT (c) Yusuf Eminoğlu. See [LICENSE](LICENSE).
@@ -0,0 +1,225 @@
1
+ # whatcrs
2
+
3
+ **What CRS is this?** Guess the coordinate reference system of mystery coordinates and get a
4
+ ranked list of EPSG codes, each with a confidence and the reasons behind it.
5
+
6
+ For everyone who has received a CSV with `X`/`Y` columns and no projection, a shapefile
7
+ without a `.prj`, a GeoJSON that lands in the Gulf of Guinea, or a colleague's
8
+ "it's in UTM, I think".
9
+
10
+ ```console
11
+ $ whatcrs 440615 4474610 --hint Spain
12
+ 1. EPSG:25830 ETRS89 / UTM zone 30N - confidence 23%
13
+ centre: 40.42000°N 3.70001°W (Spain)
14
+ WGS 84: -3.70001, 40.42000
15
+ - eastings near 500,000 and northings 4.47e+06 -> transverse Mercator / UTM pattern, northern hemisphere
16
+ - inverse projection lands at 40.4200°N, 3.7000°W in Spain
17
+ - all points inside the area of use (Europe between 6°W and 0°W: Faroe Islands offshore; ...)
18
+ - matches the hint 'Spain'
19
+ - commonly used CRS
20
+ 2. EPSG:32630 WGS 84 / UTM zone 30N - confidence 11%
21
+ ...
22
+ ```
23
+
24
+ * Works **offline with zero dependencies**: a curated set of 876 widely used CRSs and
25
+ pure-Python inverse projections that agree with PROJ to a few millimetres.
26
+ * With **pyproj** installed (`pip install whatcrs[pyproj]`) it searches every
27
+ non-deprecated EPSG projected CRS (about 5,300) and reports sample points in true WGS 84.
28
+ * Understands hints such as `"Spain"`, `"probably in Germany"`, `"Texas"`, `"Europe"`,
29
+ `"DE"` or a bounding box.
30
+ * Detects swapped axes (lat/lon vs lon/lat, northing/easting) and can fix them.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install whatcrs # bundled CRS subset, pure Python
36
+ pip install "whatcrs[pyproj]" # full EPSG database through pyproj
37
+ ```
38
+
39
+ Python 3.9+.
40
+
41
+ ## Command line
42
+
43
+ ```bash
44
+ whatcrs 512345 4182345 # one point
45
+ whatcrs 512345 4182345 513000 4183100 # several points (also "x,y" tokens)
46
+ whatcrs points.csv --x lon --y lat --hint "Spain"
47
+ whatcrs parcels.geojson --json
48
+ whatcrs "POINT (2600000 1200000)" # WKT / EWKT
49
+ cat export.csv | whatcrs - --hint Texas --units ft
50
+ whatcrs 998114 194309 --bbox=-75,40,-73,41.5 # bbox: use "=" when it starts with "-"
51
+ ```
52
+
53
+ Options: `--hint`, `--bbox`, `--units m|ft|deg`, `--x/--y` (CSV column name or 0-based
54
+ index), `--delimiter`, `--format csv|tsv|geojson|wkt`, `--top N`, `--json`,
55
+ `--no-pyproj` (bundled subset only), `--no-swap`. CSV columns named `x/y`, `lon/lat`,
56
+ `easting/northing`, `rechtswert/hochwert`... are found automatically, decimal commas are
57
+ handled, and a GeoJSON `crs` member is reported and compared with the guess. The exit code
58
+ is 0 when at least one candidate fits, 1 when none does and 2 on usage errors.
59
+
60
+ ## Python API
61
+
62
+ ```python
63
+ import whatcrs
64
+
65
+ cands = whatcrs.guess_crs([(440615, 4474610), (441000, 4475200)], hint="Spain")
66
+ best = cands[0]
67
+ best.epsg # 25830
68
+ best.code # 'EPSG:25830'
69
+ best.name # 'ETRS89 / UTM zone 30N'
70
+ best.confidence # 0.23 (softmax over all candidates)
71
+ best.axis_order # 'xy' or 'yx' (input was northing/latitude first)
72
+ best.country # 'Spain'
73
+ best.centroid # (-3.70, 40.42) lon/lat
74
+ best.sample_lonlat # first points, inverse-projected
75
+ best.reasons # list of plain-language explanations
76
+ best.to_dict() # JSON-ready
77
+
78
+ whatcrs.guess_crs(points, hint="Texas", units="ft") # State Plane feet
79
+ whatcrs.guess_crs(points, bbox=(5, 47, 15, 55), top=10) # (west, south, east, north)
80
+ whatcrs.guess_crs(points, use_pyproj=False) # bundled subset only
81
+
82
+ whatcrs.fix_swapped_axes([(52.52, 13.40), (52.50, 13.45)]) # -> [(13.40, 52.52), ...]
83
+ whatcrs.to_lonlat([(529755.8, 180719.2)], 27700) # pure-Python inverse
84
+ whatcrs.describe_coordinates(points) # what the magnitudes suggest
85
+ whatcrs.country_at(-3.70, 40.42) # 'Spain'
86
+ src = whatcrs.read_points("data.csv", x="E", y="N") # CSV / GeoJSON / WKT
87
+ src.points, src.declared_crs
88
+ ```
89
+
90
+ `fix_swapped_axes` swaps the whole data set when the best guess says the input was (y, x)
91
+ and, for geographic data, also repairs single rows whose latitude and longitude were swapped.
92
+
93
+ ## How it works
94
+
95
+ 1. **Candidates from magnitudes.** Degree-sized values (|x| <= 180, |y| <= 90) are
96
+ geographic, in either axis order. Everything else is tested against every projected CRS
97
+ in the database: the median point is inverse-projected in both axis orders and kept only
98
+ if it lands within 2° of the CRS's EPSG area of use. That single step captures the
99
+ classic patterns - eastings around 500,000 m (UTM / TM false easting), northings of
100
+ 4.2e6 m (about 38°N) or 10,000,000 m minus something (southern hemisphere), a leading
101
+ zone digit (Gauss-Krüger `4 468 746`), State Plane feet, Web Mercator ranges, swapped
102
+ columns.
103
+ 2. **Scoring.** Each surviving (CRS, axis order) pair inverse-projects up to 40 sample
104
+ points and gets a log-score built from:
105
+ * the fraction of points inside the area of use, and how far the others miss it;
106
+ * the fraction on land (Natural Earth 1:50m countries, about 10 km coastal tolerance);
107
+ * the fraction inside the hinted region;
108
+ * specificity - a CRS with a small area of use explains the observed numbers better
109
+ than a world-wide one (a likelihood term ~ 1 / area);
110
+ * a prior: commonly used CRSs (the bundled list), modern datums (ETRS89, NAD83,
111
+ GDA2020, SIRGAS 2000...) over legacy ones with identical numbers (ED50, NAD27...),
112
+ regional datums over the global WGS 84 variant, EPSG:4326 and EPSG:3857, the usual
113
+ axis order, and the `units` hint.
114
+ 3. **Confidence** is a softmax over the scores, so CRSs that are numerically
115
+ indistinguishable (ETRS89 / UTM 30N and WGS 84 / UTM 30N agree to centimetres) share it.
116
+
117
+ All projection math is pure Python (`whatcrs.projections`): transverse Mercator with
118
+ Krüger's 6th-order series (as PROJ's default `tmerc`), UTM, Lambert conformal conic
119
+ (1SP/2SP), Albers, Lambert azimuthal equal area, polar and oblique (double) stereographic,
120
+ Swiss oblique Mercator, Mercator and Pseudo-Mercator, Cassini-Soldner, American polyconic,
121
+ Krovak, Hotine oblique Mercator (variants A/B), equidistant and equal-area cylindrical,
122
+ with linear units, axis orientation (`+axis=wsu`...) and prime meridians. Checked against
123
+ PROJ 9.8 for every non-deprecated EPSG projected CRS that uses these methods (5,287 CRSs):
124
+ 5,285 agree to better than 2 mm inside their areas of use; two 19th-century Austrian
125
+ Cassini grids differ by about 250 m. Results are on the CRS's own datum; with pyproj the
126
+ reported sample points are transformed to WGS 84 including the datum shift.
127
+
128
+ ## Accuracy benchmark
129
+
130
+ `benchmarks/benchmark.py` draws 25 points in a ~30 x 30 km box around a place in each of
131
+ 52 test cases covering 50 CRSs on every continent (UTM north/south, national grids,
132
+ Gauss-Krüger, State Plane feet, Lambert, Albers, LAEA, oblique stereographic, Krovak,
133
+ Hotine, polyconic, polar stereographic, Web Mercator, lon/lat and lat/lon), projects them
134
+ with pyproj and asks `guess_crs` for the top 3 - once without a hint and once with the
135
+ country (or US state) as hint. *Exact* means the expected EPSG code; *equivalent* means a
136
+ CRS that puts every point within 50 m of its true WGS 84 position (for example ETRS89 /
137
+ UTM 30N when WGS 84 / UTM 30N was used).
138
+
139
+ | Mode (52 cases) | Top-1 exact | Top-1 equivalent | Top-3 exact | Top-3 equivalent | Time per case |
140
+ |---|---:|---:|---:|---:|---:|
141
+ | bundled subset, no hint | 31% | 33% | 38% | 40% | 0.30 s |
142
+ | bundled subset, with hint | **77%** | **83%** | **92%** | **92%** | 0.29 s |
143
+ | pyproj (all EPSG), no hint | 31% | 33% | 38% | 40% | 0.77 s |
144
+ | pyproj (all EPSG), with hint | **77%** | **83%** | **92%** | **92%** | 0.75 s |
145
+
146
+ (whatcrs 0.1.0, pyproj 3.8.0 / PROJ 9.8.1 / EPSG v12.029, Windows, Python 3.12; the first
147
+ pyproj call also spends about 1-2 s reading the EPSG database.)
148
+
149
+ Reading the numbers:
150
+
151
+ * **Give a hint.** Without one, many inputs are genuinely ambiguous: `440000 4474000` is a
152
+ valid UTM coordinate in all 60 northern zones and lands on land in many of them. A
153
+ country-level hint fixes most cases.
154
+ * The remaining hinted misses are national multi-zone systems - Japan's 19 plane
155
+ rectangular zones, China's 3-degree Gauss-Krüger CMs, California's State Plane zones,
156
+ Australia's MGA zones - whose zones share false origins, so any zone puts the points
157
+ somewhere in the same country. A state/province hint (`"New South Wales"`, `"Texas"`) or
158
+ a `bbox` resolves them.
159
+ * The bundled subset and the full EPSG database perform alike on these common CRSs; pyproj
160
+ matters for the long tail of rarer EPSG codes.
161
+
162
+ Reproduce with `python benchmarks/benchmark.py --verbose`.
163
+
164
+ ## Bundled CRS subset (no pyproj)
165
+
166
+ 876 CRSs from the EPSG dataset, including:
167
+
168
+ * geographic: WGS 84 (4326), ETRS89 (4258), NAD83 (4269), GDA94/GDA2020, SIRGAS 2000,
169
+ CGCS2000, JGD2011, NZGD2000, NAD83(CSRS);
170
+ * world / polar: Web Mercator 3857, World Mercator 3395, 4087, UPS, Antarctic and Arctic
171
+ polar stereographic (3031, 3413, 3995, 3976, 3996), EASE-Grid 2.0 (6931-6933);
172
+ * UTM: all WGS 84 zones north and south (32601-32660, 32701-32760), ETRS89 (25828-25837),
173
+ NAD83 (26901-26923), NAD27 (26703-26722), ED50 (23028-23038), SIRGAS 2000 (31965-31985),
174
+ SAD69, GDA94 / GDA2020 MGA (28348-28358, 7846-7859), JGD2011, NAD83(CSRS);
175
+ * Europe: British National Grid 27700, Irish Grid / ITM (29903, 2157), Lambert-93 2154 and
176
+ CC42-CC50, NTF Lambert II étendu 27572, RD New 28992, Swiss LV95 / LV03 (2056, 21781),
177
+ German Gauss-Krüger 31466-31469 and Soldner Berlin, Austrian GK and Lambert, Belgian
178
+ Lambert 72/2008, Luxembourg, Portugal PT-TM06, Spain, Italy (Monte Mario, RDN2008),
179
+ Greece 2100, Poland 2180 and 2176-2179, Czech/Slovak Krovak 5514, Hungary EOV 23700,
180
+ Romania Stereo70 3844, Croatia, Slovenia, Baltic states, Sweden SWEREF99, Finland
181
+ TM35FIN / GK, Iceland, Ukraine, Turkey TUREF/ED50 TM27-TM45, Pulkovo 1942 Gauss-Krüger,
182
+ ETRS89 LAEA/LCC (3035, 3034);
183
+ * Asia: China CGCS2000 Gauss-Krüger (zones and CMs), Japan plane rectangular I-XIX,
184
+ Korea, Taiwan TWD97, Hong Kong, Singapore SVY21, Malaysia RSO, Philippines PRS92,
185
+ Vietnam VN-2000, Indonesia TM-3, India NSF LCC and Kalianpur zones, Israel;
186
+ * Africa: South Africa Lo zones, Egypt belts, Morocco, Nigeria, Kenya, Ghana (Gold Coast
187
+ feet), Tunisia;
188
+ * Americas: NAD83 State Plane in metres and US feet (all states), Conus Albers 5070, US
189
+ National Atlas, Alaska Albers, Canada Atlas Lambert, Statistics Canada, MTM, BC Albers,
190
+ Quebec and Ontario Lambert, Mexico LCC, Costa Rica, Colombia (Origen Nacional and Bogotá
191
+ zones), Argentina POSGAR 98/2007, Brazil polyconic / Mercator, Venezuela, Uruguay;
192
+ * Oceania: NZTM 2193, Australian Albers 3577, GA Lambert, NSW Lambert, VicGrid, Fiji.
193
+
194
+ `whatcrs.builtin_records()` lists them; `tools/build_data.py` regenerates the table.
195
+
196
+ ## Limitations
197
+
198
+ * Coordinates alone cannot separate CRSs that share projection parameters and differ only
199
+ by datum (tens to hundreds of metres); the ranking prefers the modern regional datum.
200
+ * Bundled country outlines are simplified (about 3 km) Natural Earth 1:50m polygons; points
201
+ within about 10 km of a coast count as land.
202
+ * Not handled: vertical CRSs, local engineering grids, degrees-minutes-seconds strings,
203
+ coordinates scaled by an unknown factor.
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ pip install -e ".[dev,pyproj]"
209
+ python -m pytest
210
+ python -m ruff check . && python -m ruff format --check src tests tools benchmarks
211
+ python -m mypy src
212
+ python benchmarks/benchmark.py
213
+ ```
214
+
215
+ ## Data credits
216
+
217
+ * Country outlines and state/province boxes: **Made with Natural Earth.** Free vector and
218
+ raster map data @ [naturalearthdata.com](https://www.naturalearthdata.com/) (public
219
+ domain), 1:50m Admin 0 countries and Admin 1 states/provinces.
220
+ * CRS definitions and areas of use: the [EPSG Geodetic Parameter Dataset](https://epsg.org)
221
+ (IOGP), as distributed with [PROJ](https://proj.org).
222
+
223
+ ## License
224
+
225
+ MIT (c) Yusuf Eminoğlu. See [LICENSE](LICENSE).