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.
- whatcrs-0.1.0/CHANGELOG.md +35 -0
- whatcrs-0.1.0/CITATION.cff +19 -0
- whatcrs-0.1.0/LICENSE +21 -0
- whatcrs-0.1.0/MANIFEST.in +11 -0
- whatcrs-0.1.0/PKG-INFO +261 -0
- whatcrs-0.1.0/README.md +225 -0
- whatcrs-0.1.0/benchmarks/benchmark.py +192 -0
- whatcrs-0.1.0/pyproject.toml +119 -0
- whatcrs-0.1.0/setup.cfg +4 -0
- whatcrs-0.1.0/src/whatcrs/__init__.py +41 -0
- whatcrs-0.1.0/src/whatcrs/__main__.py +3 -0
- whatcrs-0.1.0/src/whatcrs/cli.py +192 -0
- whatcrs-0.1.0/src/whatcrs/core.py +686 -0
- whatcrs-0.1.0/src/whatcrs/crs_db.py +289 -0
- whatcrs-0.1.0/src/whatcrs/data/NOTICE.md +10 -0
- whatcrs-0.1.0/src/whatcrs/data/countries.json.gz +0 -0
- whatcrs-0.1.0/src/whatcrs/data/crs_builtin.json.gz +0 -0
- whatcrs-0.1.0/src/whatcrs/projections.py +1056 -0
- whatcrs-0.1.0/src/whatcrs/py.typed +0 -0
- whatcrs-0.1.0/src/whatcrs/readers.py +297 -0
- whatcrs-0.1.0/src/whatcrs/regions.py +435 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/PKG-INFO +261 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/SOURCES.txt +32 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/dependency_links.txt +1 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/entry_points.txt +2 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/requires.txt +9 -0
- whatcrs-0.1.0/src/whatcrs.egg-info/top_level.txt +1 -0
- whatcrs-0.1.0/tests/test_core.py +220 -0
- whatcrs-0.1.0/tests/test_projections.py +166 -0
- whatcrs-0.1.0/tests/test_pyproj_mode.py +52 -0
- whatcrs-0.1.0/tests/test_readers_cli.py +190 -0
- whatcrs-0.1.0/tests/test_regions.py +111 -0
- whatcrs-0.1.0/tests/test_vs_pyproj.py +111 -0
- 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).
|
whatcrs-0.1.0/README.md
ADDED
|
@@ -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).
|