geofix 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 (43) hide show
  1. geofix-0.1.0/.pre-commit-hooks.yaml +17 -0
  2. geofix-0.1.0/CHANGELOG.md +31 -0
  3. geofix-0.1.0/CITATION.cff +19 -0
  4. geofix-0.1.0/LICENSE +21 -0
  5. geofix-0.1.0/MANIFEST.in +10 -0
  6. geofix-0.1.0/PKG-INFO +382 -0
  7. geofix-0.1.0/README.md +342 -0
  8. geofix-0.1.0/examples/bad.geojson +8 -0
  9. geofix-0.1.0/examples/good.geojson +81 -0
  10. geofix-0.1.0/examples/points.geojsonl +3 -0
  11. geofix-0.1.0/pyproject.toml +131 -0
  12. geofix-0.1.0/setup.cfg +4 -0
  13. geofix-0.1.0/src/geofix/__init__.py +59 -0
  14. geofix-0.1.0/src/geofix/__main__.py +7 -0
  15. geofix-0.1.0/src/geofix/api.py +359 -0
  16. geofix-0.1.0/src/geofix/cli.py +350 -0
  17. geofix-0.1.0/src/geofix/config.py +102 -0
  18. geofix-0.1.0/src/geofix/extent.py +121 -0
  19. geofix-0.1.0/src/geofix/geometry.py +649 -0
  20. geofix-0.1.0/src/geofix/model.py +90 -0
  21. geofix-0.1.0/src/geofix/options.py +40 -0
  22. geofix-0.1.0/src/geofix/py.typed +0 -0
  23. geofix-0.1.0/src/geofix/reader.py +258 -0
  24. geofix-0.1.0/src/geofix/repair.py +681 -0
  25. geofix-0.1.0/src/geofix/report.py +180 -0
  26. geofix-0.1.0/src/geofix/rules.py +349 -0
  27. geofix-0.1.0/src/geofix/stats.py +244 -0
  28. geofix-0.1.0/src/geofix/validate.py +903 -0
  29. geofix-0.1.0/src/geofix.egg-info/PKG-INFO +382 -0
  30. geofix-0.1.0/src/geofix.egg-info/SOURCES.txt +41 -0
  31. geofix-0.1.0/src/geofix.egg-info/dependency_links.txt +1 -0
  32. geofix-0.1.0/src/geofix.egg-info/entry_points.txt +2 -0
  33. geofix-0.1.0/src/geofix.egg-info/requires.txt +12 -0
  34. geofix-0.1.0/src/geofix.egg-info/top_level.txt +1 -0
  35. geofix-0.1.0/tests/conftest.py +56 -0
  36. geofix-0.1.0/tests/test_api_files.py +243 -0
  37. geofix-0.1.0/tests/test_cli.py +238 -0
  38. geofix-0.1.0/tests/test_geometry_kernel.py +183 -0
  39. geofix-0.1.0/tests/test_performance.py +114 -0
  40. geofix-0.1.0/tests/test_reader.py +143 -0
  41. geofix-0.1.0/tests/test_repair.py +292 -0
  42. geofix-0.1.0/tests/test_rules_geometry.py +378 -0
  43. geofix-0.1.0/tests/test_rules_structure.py +279 -0
@@ -0,0 +1,17 @@
1
+ - id: geofix
2
+ name: geofix (validate GeoJSON)
3
+ description: Validate GeoJSON / GeoJSONSeq files against RFC 7946.
4
+ entry: geofix check
5
+ language: python
6
+ files: '\.(geojson|geojsonl|geojsons|geojsonseq|ndgeojson)$'
7
+ types: [file]
8
+ require_serial: false
9
+
10
+ - id: geofix-fix
11
+ name: geofix (repair GeoJSON)
12
+ description: Repair GeoJSON files in place (winding, closing rings, duplicates, bbox, ...).
13
+ entry: geofix fix --in-place
14
+ language: python
15
+ files: '\.(geojson|geojsonl|geojsons|geojsonseq|ndgeojson)$'
16
+ types: [file]
17
+ require_serial: false
@@ -0,0 +1,31 @@
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
+ - **Validator** for RFC 7946 GeoJSON with 38 rules (GF000-GF302): document structure,
10
+ member types, ids, foreign/reserved members and `crs`; position arity, NaN/Infinity,
11
+ longitude/latitude ranges, swapped axes, projected-coordinate detection with EPSG:3857
12
+ back-projection and UTM/TM hints, excessive precision, duplicate vertices, mixed
13
+ dimensions; empty/null geometries, short lines, unclosed/short/zero-area rings, winding
14
+ order (right-hand rule), antimeridian jumps; `bbox` validity, containment and tightness.
15
+ Every issue has a code, severity, JSON path, line, message and fix hint.
16
+ - **Pure-Python topology checks**: ring self-intersections (crossings, touches, spikes),
17
+ holes outside or crossing their shell, crossing/overlapping/nested holes. Uses a convexity
18
+ shortcut, monotone-chain bisection and a uniform grid; no shapely or GEOS required.
19
+ - **Repairer** (`geofix fix`, `geofix.fix`, `geofix.fix_file`): rounding, duplicate removal,
20
+ ring closing, degenerate hole/part removal, bow-tie splitting in pure Python,
21
+ `shapely.make_valid` for the rest (optional `shapely` extra), rewinding, bbox
22
+ recomputation, removal of empty features and of WGS 84 `crs` members, missing
23
+ `properties`/`geometry` members. Every modification is reported as a `Change`; repairs are
24
+ idempotent and valid files are left byte-for-byte untouched.
25
+ - **Streaming reader** for FeatureCollections (feature by feature) and newline-delimited
26
+ GeoJSON / GeoJSONSeq (RFC 8142 record separators), with line numbers and recovery from bad
27
+ records.
28
+ - **CLI** `geofix check|fix|stats|rules` with exit codes for CI, `--format text|json|github`
29
+ (GitHub Actions annotations), `--select`/`--ignore`, `--fail-on`, `--precision`,
30
+ directory expansion, and `[tool.geofix]` configuration in `pyproject.toml`.
31
+ - `.pre-commit-hooks.yaml` with `geofix` (check) and `geofix-fix` (repair in place) hooks.
@@ -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: "geofix"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-08"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/geofix_sdk"
13
+ url: "https://pypi.org/project/geofix/"
14
+ abstract: "Fast, dependency-light GeoJSON (RFC 7946) validator and repairer for local runs, pre-commit hooks and CI."
15
+ keywords:
16
+ - geojson
17
+ - rfc7946
18
+ - validation
19
+ - gis
geofix-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,10 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include .pre-commit-hooks.yaml
7
+ include src/geofix/py.typed
8
+ recursive-include src/geofix *.py
9
+ recursive-include tests *.py
10
+ recursive-include examples *.geojson *.geojsonl
geofix-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.4
2
+ Name: geofix
3
+ Version: 0.1.0
4
+ Summary: Fast, dependency-light GeoJSON (RFC 7946) validator and repairer for local runs, pre-commit and CI.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/geofix_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/geofix_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/geofix_sdk/issues
10
+ Project-URL: Documentation, https://github.com/YusufEminoglu/geofix_sdk#readme
11
+ Project-URL: Changelog, https://github.com/YusufEminoglu/geofix_sdk/blob/main/CHANGELOG.md
12
+ Keywords: geojson,rfc7946,validation,validator,linter,repair,make-valid,gis,geospatial,topology,self-intersection,winding-order,pre-commit,github-actions,ci,geojsonseq,ndjson
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Classifier: Environment :: Console
25
+ Classifier: Intended Audience :: Science/Research
26
+ Classifier: Intended Audience :: Developers
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: tomli>=1.1.0; python_version < "3.11"
32
+ Provides-Extra: shapely
33
+ Requires-Dist: shapely>=2.0.0; extra == "shapely"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
36
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
38
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # geofix
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/geofix.svg)](https://pypi.org/project/geofix/)
44
+ [![Python](https://img.shields.io/pypi/pyversions/geofix.svg)](https://pypi.org/project/geofix/)
45
+ [![CI](https://github.com/YusufEminoglu/geofix_sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/YusufEminoglu/geofix_sdk/actions/workflows/ci.yml)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
47
+
48
+ **Fast, dependency-light GeoJSON validator and repairer** for the command line, pre-commit
49
+ hooks and CI. It checks files against [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)
50
+ (structure, coordinates, winding, bounding boxes) and polygon topology (self-intersections,
51
+ holes), tells you *where* each problem is, and can repair most of them, reporting every change
52
+ it makes.
53
+
54
+ - **Pure Python, no required dependencies** (only `tomli` on Python < 3.11 for reading
55
+ `pyproject.toml`). Topology checks run without shapely or GEOS.
56
+ - **Streams large files**: a `FeatureCollection` is read one feature at a time, and
57
+ newline-delimited GeoJSON (GeoJSONSeq / RFC 8142, `.geojsonl`, `.ndgeojson`) one record at a
58
+ time.
59
+ - **Made for CI**: exit codes, `--format github` annotations, `--format json`,
60
+ `--select`/`--ignore` rule codes, `[tool.geofix]` configuration, a pre-commit hook.
61
+ - **Explains CRS mistakes**: "this looks like EPSG:3857 Web Mercator (the centre would be
62
+ lon 28.99762, lat 40.97734)", UTM/TM eastings, swapped `[lat, lon]` axes.
63
+ - **Repairs safely**: `geofix fix` never changes data without a recorded change, leaves valid
64
+ files byte-for-byte untouched, and is idempotent.
65
+
66
+ ```console
67
+ pip install geofix # pure Python
68
+ pip install "geofix[shapely]" # + shapely.make_valid for complex topology repairs
69
+ ```
70
+
71
+ ## Command line
72
+
73
+ ### `geofix check`
74
+
75
+ ```console
76
+ $ geofix check examples/bad.geojson
77
+ examples/bad.geojson:2:1: GF212 warning: exterior ring is clockwise [$.features[0].geometry.coordinates[0]]
78
+ hint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
79
+ examples/bad.geojson:3:1: GF220 error: ring crosses itself at (29.025, 41.005) [$.features[1].geometry.coordinates[0]]
80
+ hint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.
81
+ examples/bad.geojson:4:1: GF109 warning: 1 consecutive duplicate position(s), first [29.05, 41] [$.features[2].geometry.coordinates[0][2]]
82
+ hint: Remove the repeated positions.
83
+ examples/bad.geojson:4:1: GF210 error: ring is not closed: first position [29.04, 41], last [29.04, 41.01] [$.features[2].geometry.coordinates[0]]
84
+ hint: Repeat the first position at the end of the ring.
85
+ examples/bad.geojson:4:1: GF108 info: 1 of 5 position(s) have more than 7 decimals [$.features[2].geometry.coordinates[0][3]]
86
+ hint: Round coordinates; 6 decimals is ~10 cm, 7 is ~1 cm (RFC 7946 section 11.2).
87
+ examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
88
+ hint: Give every feature its own id, or drop ids that are not identifiers.
89
+ examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
90
+ hint: Reproject to EPSG:4326, e.g. ogr2ogr -t_srs EPSG:4326 -lco RFC7946=YES out.geojson in.geojson.
91
+ examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]
92
+ hint: GeoJSON order is longitude first; swap x and y (EPSG:4326 axis order is a common cause).
93
+ examples/bad.geojson:7:1: GF200 warning: MultiPolygon has empty coordinates [$.features[5].geometry]
94
+ hint: Remove the feature, or use "geometry": null for features without a location.
95
+ examples/bad.geojson:1:1: GF301 error: "bbox" [29, 41, 29.03, 41.01] does not contain the coordinates (extent [29, 41, 3228000.5, 5009000.25]) [$.bbox]
96
+ hint: Recompute the bbox from the coordinates.
97
+
98
+ Found 10 issues (4 errors, 5 warnings, 1 info) in 1 of 1 file.
99
+ $ echo $?
100
+ 1
101
+ ```
102
+
103
+ Each line is `file:line:column: CODE severity: message [JSON path]`. The line is where the
104
+ feature (or record) starts; the JSON path points inside it. Directories are searched
105
+ recursively for `.geojson`, `.geojsonl`, `.geojsons`, `.geojsonseq`, `.ndgeojson` and
106
+ `.geo.json` files; `-` reads standard input.
107
+
108
+ Useful options: `--select GF2,GF3`, `--ignore GF007,GF108` (codes or prefixes, `ALL`),
109
+ `--fail-on error|warning|info`, `--precision N`, `--statistics`, `--no-hints`,
110
+ `--format text|json|github`.
111
+
112
+ ### `geofix fix`
113
+
114
+ ```console
115
+ $ geofix fix examples/bad.geojson -o fixed.geojson --no-hints
116
+ examples/bad.geojson: fixed 9 problems (GF108 x1, GF109 x1, GF200 x1, GF210 x1, GF212 x2, GF220 x2, GF301 x1)
117
+ examples/bad.geojson:5:1: GF006 warning: id 'parcel-3' is also used by $.features[2] [$.features[3].id]
118
+ examples/bad.geojson:5:1: GF107 error: coordinates (x 3.228e+06..3.228e+06, y 5.009e+06..5.009e+06) look projected, not WGS 84 longitude/latitude: this looks like EPSG:3857 Web Mercator (the centre would be lon 28.99762, lat 40.97734) or a Gauss-Kruger/TM grid with zone-prefixed eastings [$.features[3].geometry.coordinates]
119
+ examples/bad.geojson:6:1: GF106 warning: latitude 129.0 is out of range while every longitude is within +/-90: positions look like [lat, lon] [$.features[4].geometry.coordinates[0]]
120
+
121
+ Applied 9 fixes to 1 file. Found 3 remaining issues (1 error, 2 warnings) in 1 of 1 file.
122
+ ```
123
+
124
+ What it did: rewound the clockwise shell, split the bow-tie into a two-part MultiPolygon,
125
+ removed the duplicate vertex, closed the open ring, rounded the over-precise coordinate,
126
+ removed the feature with an empty geometry and recomputed the collection `bbox`. It did
127
+ **not** guess a CRS or swap axes: those are reported for a human to decide.
128
+
129
+ - `geofix fix FILE` writes the result to standard output (the report goes to stderr);
130
+ `-o OUT` writes a file; `-i/--in-place` rewrites inputs **only when something changed**;
131
+ `--dry-run` reports what would change and exits 1 if anything would.
132
+ - `--show-changes` lists every change instead of a per-file summary;
133
+ `--exit-nonzero-on-fix` exits 1 when a file was modified; `--no-shapely` keeps repairs pure
134
+ Python.
135
+ - Collections are written one feature per line (readable diffs); `--indent N` pretty-prints
136
+ single objects. Collection members keep their order, except `bbox`, which is written after
137
+ `features` because it can only be computed once every feature has been repaired.
138
+ - Repairs run in this order: round (GF108), drop duplicates (GF109), close rings (GF210),
139
+ drop degenerate holes/parts (GF211/GF214), topology (GF220-GF222), rewind (GF212/GF213),
140
+ recompute bboxes (GF300-GF302). A second `geofix fix` on the output changes nothing.
141
+ - Rounding to the configured precision (7 decimals by default, ~1 cm) is a repair like any
142
+ other: disable it with `--ignore GF108` or change it with `--precision 9`.
143
+
144
+ ### `geofix stats` and `geofix rules`
145
+
146
+ ```console
147
+ $ geofix stats examples/bad.geojson
148
+ examples/bad.geojson
149
+ kind: FeatureCollection
150
+ size: 1.2 kB
151
+ features: 6
152
+ geometry types: Polygon 3, Point 1, LineString 1, MultiPolygon 1
153
+ null / empty: 0 / 1
154
+ positions: 18 (0 with z)
155
+ max decimals: 10
156
+ bbox: [29, 41, 3.228e+06, 5.009e+06]
157
+ ids: 6 (5 unique)
158
+ properties: name (5)
159
+ read in: 0.00 s
160
+ ```
161
+
162
+ `geofix rules` lists every rule (`--format text|json|markdown`).
163
+
164
+ ### Exit codes
165
+
166
+ | Status | Meaning |
167
+ |---|---|
168
+ | 0 | No issue at or above `--fail-on` (default `error`) |
169
+ | 1 | Issues at or above `--fail-on`; also `fix --dry-run` / `--exit-nonzero-on-fix` when a fix is (or was) needed |
170
+ | 2 | Usage or configuration error, or an input that cannot be read |
171
+
172
+ Warnings do not fail a run by default; use `--fail-on warning` (or `fail-on = "warning"` in
173
+ the configuration) for strict RFC 7946 conformance, e.g. ring winding.
174
+
175
+ ## Rules
176
+
177
+ | Code | Name | Severity | Checks | `geofix fix` |
178
+ |---|---|---|---|---|
179
+ | GF000 | invalid-json | error | The input is not valid UTF-8 JSON. | report only |
180
+ | GF001 | invalid-type | error | Missing or unknown "type" member. | report only |
181
+ | GF002 | missing-member | error | A required member is missing (Feature "geometry"/"properties", FeatureCollection "features", geometry "coordinates", GeometryCollection "geometries"). | Adds "properties": {} or "geometry": null to features that lack them. |
182
+ | GF003 | invalid-member | error | A member has the wrong JSON type (e.g. "features" is not an array, a feature is not a Feature object, "geometry" is not an object or null). | report only |
183
+ | GF004 | invalid-properties | error | "properties" must be a JSON object or null. | report only |
184
+ | GF005 | invalid-id | error | Feature "id" must be a string or a number. | report only |
185
+ | GF006 | duplicate-id | warning | Feature "id" is not unique within the collection. | report only |
186
+ | GF007 | foreign-member | info | Member that RFC 7946 does not define for this object (a foreign member). | report only |
187
+ | GF008 | reserved-member | error | Member that changes GeoJSON semantics on this object type (RFC 7946 section 7.1), e.g. "coordinates" on a Feature or "properties" on a geometry. | report only |
188
+ | GF009 | crs-member | warning | "crs" member present; RFC 7946 removed it and requires WGS 84 longitude/latitude. | Removes "crs" when it already names WGS 84 (CRS84 / EPSG:4326). |
189
+ | GF010 | nested-geometry-collection | warning | GeometryCollection nested inside another GeometryCollection (RFC 7946 3.1.8 says SHOULD avoid). | report only |
190
+ | GF100 | invalid-coordinates | error | "coordinates" has the wrong nesting depth or holds non-numeric values. | report only |
191
+ | GF101 | position-too-short | error | Position has fewer than two numbers. | report only |
192
+ | GF102 | position-too-long | warning | Position has more than three numbers (RFC 7946 3.1.1: SHOULD NOT). | report only |
193
+ | GF103 | non-finite-coordinate | error | Coordinate is NaN or Infinity (non-standard JSON literals, or overflowing numbers). | report only |
194
+ | GF104 | longitude-out-of-range | error | Longitude outside [-180, 180]. | report only |
195
+ | GF105 | latitude-out-of-range | error | Latitude outside [-90, 90]. | report only |
196
+ | GF106 | swapped-axes | warning | Coordinates look like [lat, lon] instead of [lon, lat]. | report only |
197
+ | GF107 | projected-coordinates | error | Coordinates look projected (metres: EPSG:3857, UTM, national TM grids), not WGS 84. | report only |
198
+ | GF108 | excessive-precision | info | Coordinates carry more decimals than the configured precision (default 7, ~1 cm). | Rounds longitude/latitude to the configured precision. |
199
+ | GF109 | duplicate-vertex | warning | Consecutive duplicate positions in a line or ring. | Drops consecutive duplicate positions. |
200
+ | GF110 | mixed-dimensions | info | Geometry mixes 2D and 3D positions. | report only |
201
+ | GF200 | empty-geometry | warning | Geometry has no coordinates (or a GeometryCollection has no geometries). | Removes features whose geometry is empty from collections and sequences. |
202
+ | GF201 | null-geometry | info | Feature has "geometry": null (allowed, but many tools skip such features). | report only |
203
+ | GF202 | line-too-short | error | LineString has fewer than two positions. | report only |
204
+ | GF203 | zero-length-line | warning | LineString whose positions are all identical. | report only |
205
+ | GF210 | unclosed-ring | error | Linear ring's first and last positions differ. | Appends the first position to close the ring. |
206
+ | GF211 | ring-too-short | error | Linear ring has fewer than four positions. | Drops degenerate holes and polygon parts (always reported). |
207
+ | GF212 | exterior-clockwise | warning | Exterior ring is clockwise; RFC 7946 3.1.6 requires counter-clockwise. | Reverses the ring. |
208
+ | GF213 | hole-counterclockwise | warning | Interior ring (hole) is counter-clockwise; RFC 7946 3.1.6 requires clockwise. | Reverses the ring. |
209
+ | GF214 | zero-area-ring | error | Linear ring encloses no area (all positions collinear or collapsed). | Drops degenerate holes and polygon parts (always reported). |
210
+ | GF220 | self-intersection | error | Ring crosses or touches itself. | Splits single-crossing (bow-tie) rings in pure Python; other cases use shapely.make_valid when shapely is installed. |
211
+ | GF221 | hole-outside-shell | error | Hole is not inside its exterior ring (lies outside it or crosses it). | Rebuilt with shapely.make_valid when shapely is installed. |
212
+ | GF222 | holes-overlap | error | Two holes of the same polygon cross, overlap or nest. | Rebuilt with shapely.make_valid when shapely is installed. |
213
+ | GF230 | antimeridian-crossing | warning | Consecutive positions jump more than 180 degrees of longitude; the segment probably crosses the antimeridian. | report only |
214
+ | GF300 | invalid-bbox | error | "bbox" is not an array of 2*n numbers with min <= max (west > east is allowed across the antimeridian). | Recomputes the bbox from the coordinates. |
215
+ | GF301 | bbox-mismatch | error | "bbox" does not contain all coordinates of the object. | Recomputes the bbox from the coordinates. |
216
+ | GF302 | bbox-not-tight | info | "bbox" is larger than the extent of the coordinates. | Recomputes the bbox from the coordinates. |
217
+
218
+ Notes on the checks:
219
+
220
+ - **Topology** is planar, in the coordinate space of the file, like the OGC simple feature
221
+ rules: within a ring any contact between non-adjacent edges is an error (crossing,
222
+ touching, or a collinear spike); between rings, crossings and shared edges are errors,
223
+ while a single touching point is allowed. Overlap between the *parts* of a MultiPolygon is
224
+ not checked.
225
+ - **Antimeridian**: RFC 7946 3.1.9 asks for geometries crossing 180 degrees to be cut into
226
+ multi-part geometries. GF230 flags segments that jump more than 180 degrees of longitude;
227
+ geofix does not cut them (use `ogr2ogr -wrapdateline` or the `antimeridian` package). A
228
+ `bbox` with west > east is accepted as an antimeridian-crossing box (RFC 7946 5.2).
229
+ - **CRS heuristics** (GF106/GF107) look at each geometry's extent. Values beyond +/-1000 are
230
+ treated as metres; eastings of 100 000-900 000 m suggest UTM or a national TM grid (for
231
+ example TUREF/TM), values within the Web Mercator square are back-projected so you can see
232
+ where the data would land if it were EPSG:3857. `[lat, lon]` is only detectable when a
233
+ latitude falls outside +/-90 while every longitude is within +/-90.
234
+ - **Precision** (GF108) counts decimals of the shortest representation of each longitude and
235
+ latitude (elevations are not checked).
236
+
237
+ ## Configuration
238
+
239
+ `geofix` reads `[tool.geofix]` from the nearest `pyproject.toml` (or `--config FILE`;
240
+ `--isolated` ignores it). Command-line options override it.
241
+
242
+ ```toml
243
+ [tool.geofix]
244
+ select = ["ALL"] # codes or prefixes, e.g. ["GF0", "GF2"]
245
+ ignore = ["GF007"] # foreign members are fine for us
246
+ fail-on = "warning" # error | warning | info
247
+ precision = 6 # GF108 threshold and fix rounding
248
+ shapely = true # let `geofix fix` use shapely.make_valid when installed
249
+ format = "text" # text | json | github
250
+ ```
251
+
252
+ ## pre-commit
253
+
254
+ ```yaml
255
+ # .pre-commit-config.yaml
256
+ repos:
257
+ - repo: https://github.com/YusufEminoglu/geofix_sdk
258
+ rev: v0.1.0
259
+ hooks:
260
+ - id: geofix # validate
261
+ # - id: geofix-fix # or repair in place (the commit stops if a file changed)
262
+ ```
263
+
264
+ Both hooks run on `.geojson`, `.geojsonl`, `.geojsons`, `.geojsonseq` and `.ndgeojson` files
265
+ and honour `[tool.geofix]`. Pass options with `args`, e.g. `args: [--fail-on, warning]`.
266
+
267
+ ## GitHub Actions
268
+
269
+ ```yaml
270
+ # .github/workflows/geojson.yml
271
+ name: GeoJSON
272
+ on: [push, pull_request]
273
+ jobs:
274
+ geofix:
275
+ runs-on: ubuntu-latest
276
+ steps:
277
+ - uses: actions/checkout@v6
278
+ - uses: actions/setup-python@v6
279
+ with:
280
+ python-version: "3.13"
281
+ - run: pip install geofix
282
+ - run: geofix check --format github data/
283
+ ```
284
+
285
+ `--format github` prints [workflow commands](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions)
286
+ so issues appear as annotations on the pull request diff, at the line where each feature
287
+ starts:
288
+
289
+ ```text
290
+ ::warning file=examples/bad.geojson,line=2,col=1,title=geofix GF212 (exterior-clockwise)::exterior ring is clockwise at $.features[0].geometry.coordinates[0]%0Ahint: Reverse the ring (ogr2ogr -lco RFC7946=YES writes the right order).
291
+ ::error file=examples/bad.geojson,line=3,col=1,title=geofix GF220 (self-intersection)::ring crosses itself at (29.025, 41.005) at $.features[1].geometry.coordinates[0]%0Ahint: Split bow-ties into separate polygons or rebuild with shapely.make_valid.
292
+ ```
293
+
294
+ ## Python API
295
+
296
+ ```python
297
+ import geofix
298
+
299
+ # Validate a parsed object, a JSON string/bytes, or a file (streamed).
300
+ issues = geofix.check({"type": "Point", "coordinates": [3228000.0, 5009000.0]})
301
+ issues = geofix.check_file("parcels.geojson", ignore=["GF007"], precision=6)
302
+ for issue in issues:
303
+ print(issue.code, issue.severity, issue.line, issue.path, issue.message, issue.hint)
304
+
305
+ # Repair. The input is never modified; every modification is a Change.
306
+ result = geofix.fix(my_feature_collection)
307
+ result.data # repaired object
308
+ result.changes # [Change(code="GF212", path="$.features[0]...", message="rewound ...")]
309
+ result.issues # what is still wrong after the repair
310
+
311
+ # Repair a file feature by feature (dst may equal src; None is a dry run; "-" is stdout).
312
+ result = geofix.fix_file("in.geojson", "out.geojson", use_shapely=False)
313
+
314
+ # Options can be shared.
315
+ opts = geofix.Options(select=["GF2", "GF3"], fail_on="warning", precision=7)
316
+ geofix.check_file("in.geojson", opts)
317
+
318
+ # Streaming building blocks.
319
+ with open("big.geojsonl", encoding="utf-8") as fh:
320
+ for path, feature in geofix.iter_features(fh):
321
+ ...
322
+
323
+ stats = geofix.stats_file("in.geojson") # counts, bbox, property keys, max decimals
324
+ geofix.RULES["GF220"].summary # rule registry
325
+ ```
326
+
327
+ `geofix.Validator` and `geofix.Repairer` are the incremental engines behind these functions
328
+ (feed them features one at a time); `geofix.Reader` produces the streaming events.
329
+
330
+ ## Performance
331
+
332
+ Pure Python 3.12 on an AMD Ryzen 4000-series laptop CPU (single thread, Windows), synthetic
333
+ files (numbers vary with hardware; the test-suite only asserts generous limits):
334
+
335
+ | File | Size | Positions | `check` | `fix --dry-run` |
336
+ |---|---|---|---|---|
337
+ | 100 000 convex polygons (10 vertices) | 37.6 MB | 1.10 M | 5.1 s | 5.5 s |
338
+ | 100 000 non-convex polygons, 10% with holes | 44.1 MB | 1.37 M | 7.9 s | 8.2 s |
339
+ | one ring, 1 000 000 vertices (ellipse) | 25.8 MB | 1.00 M | 4.6 s | 4.6 s |
340
+ | one ring, 1 000 000 vertices (wavy, 1 000 lobes) | 25.8 MB | 1.00 M | 8.1 s | 7.9 s |
341
+
342
+ Repairing every feature of the 100 000-polygon file (all clockwise and over-precise: 200 000
343
+ changes) and writing the result takes about 21 s; a second run finds nothing to do in 5.4 s.
344
+
345
+ How: positions are validated with C-level `map`/`itemgetter` passes; the precision test is a
346
+ regex over the `repr` of a whole coordinate list (exact `round()` only runs when it matches);
347
+ convex rings skip the segment search; larger rings are split into x/y-monotone chains (whose
348
+ own segments cannot meet) and only chains with overlapping boxes are bisected against each
349
+ other, as in GEOS, with a uniform grid for zig-zag data. Memory is bounded by the largest
350
+ single feature, not the file.
351
+
352
+ ## How geofix compares
353
+
354
+ | | geofix | [@mapbox/geojsonhint](https://github.com/mapbox/geojsonhint) | [geojson](https://pypi.org/project/geojson/) (Python) | [shapely](https://pypi.org/project/shapely/) `is_valid` / `make_valid` | `ogr2ogr -lco RFC7946=YES` |
355
+ |---|---|---|---|---|---|
356
+ | Runtime | Python, no required deps | Node.js | Python | Python + GEOS (bundled in wheels) | GDAL |
357
+ | RFC 7946 structure checks | yes | yes | partial (`is_valid` / `errors()`) | no (geometry only) | no (it converts) |
358
+ | Winding order (right-hand rule) | check + fix | check | no | no (`orient` can fix) | writes CCW/CW |
359
+ | Self-intersections, holes | check; fix bow-ties, rest via shapely | no | no | yes (GEOS) | no |
360
+ | bbox, ids, foreign members, `crs` | yes | partly | no | no | writes a new file |
361
+ | Projected-CRS / swapped-axes hints | yes | no | no | no | n/a (reprojects when told the CRS) |
362
+ | Reports what was changed | yes (per change) | n/a | n/a | no | no |
363
+ | Streams large files / GeoJSONSeq | yes | no | no | n/a | yes |
364
+ | CI output (exit codes, GitHub annotations) | yes | CLI only | no | no | no |
365
+
366
+ Use shapely/GEOS when you need full OGC validity semantics on arbitrary geometries (geofix
367
+ calls `shapely.make_valid` for cases it cannot repair itself), and GDAL when you need to
368
+ reproject or convert formats; geofix is the lightweight gate that tells you which of those
369
+ you need, in a form CI understands.
370
+
371
+ ## Development
372
+
373
+ ```console
374
+ python -m pip install -e ".[dev,shapely]"
375
+ python -m pytest
376
+ python -m ruff check . && python -m ruff format --check src tests
377
+ python -m mypy src
378
+ ```
379
+
380
+ ## License
381
+
382
+ MIT, see [LICENSE](LICENSE). Citation metadata: [CITATION.cff](CITATION.cff).