parcelform 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 (33) hide show
  1. parcelform-0.1.0/CHANGELOG.md +30 -0
  2. parcelform-0.1.0/CITATION.cff +23 -0
  3. parcelform-0.1.0/LICENSE +21 -0
  4. parcelform-0.1.0/MANIFEST.in +9 -0
  5. parcelform-0.1.0/PKG-INFO +273 -0
  6. parcelform-0.1.0/README.md +238 -0
  7. parcelform-0.1.0/examples/quickstart.py +65 -0
  8. parcelform-0.1.0/pyproject.toml +113 -0
  9. parcelform-0.1.0/setup.cfg +4 -0
  10. parcelform-0.1.0/src/parcelform/__init__.py +130 -0
  11. parcelform-0.1.0/src/parcelform/__main__.py +7 -0
  12. parcelform-0.1.0/src/parcelform/_common.py +211 -0
  13. parcelform-0.1.0/src/parcelform/_version.py +1 -0
  14. parcelform-0.1.0/src/parcelform/analysis.py +217 -0
  15. parcelform-0.1.0/src/parcelform/cli.py +190 -0
  16. parcelform-0.1.0/src/parcelform/envelope.py +306 -0
  17. parcelform-0.1.0/src/parcelform/frontage.py +485 -0
  18. parcelform-0.1.0/src/parcelform/geometry.py +382 -0
  19. parcelform-0.1.0/src/parcelform/io.py +155 -0
  20. parcelform-0.1.0/src/parcelform/py.typed +0 -0
  21. parcelform-0.1.0/src/parcelform/shape.py +179 -0
  22. parcelform-0.1.0/src/parcelform/subdivision.py +286 -0
  23. parcelform-0.1.0/src/parcelform.egg-info/PKG-INFO +273 -0
  24. parcelform-0.1.0/src/parcelform.egg-info/SOURCES.txt +31 -0
  25. parcelform-0.1.0/src/parcelform.egg-info/dependency_links.txt +1 -0
  26. parcelform-0.1.0/src/parcelform.egg-info/entry_points.txt +2 -0
  27. parcelform-0.1.0/src/parcelform.egg-info/requires.txt +7 -0
  28. parcelform-0.1.0/src/parcelform.egg-info/top_level.txt +1 -0
  29. parcelform-0.1.0/tests/conftest.py +33 -0
  30. parcelform-0.1.0/tests/test_envelope_subdivision.py +183 -0
  31. parcelform-0.1.0/tests/test_frontage.py +156 -0
  32. parcelform-0.1.0/tests/test_geometry_shape.py +174 -0
  33. parcelform-0.1.0/tests/test_io_cli.py +212 -0
@@ -0,0 +1,30 @@
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-15
8
+ ### Added
9
+ - Planar geometry: area, perimeter (with holes), centroid, convex hull (monotone chain),
10
+ minimum-area rectangle by rotating calipers, minimum enclosing circle (Welzl),
11
+ point-in-polygon, half-plane clipping.
12
+ - Shape indices: Polsby-Popper, Schwartzberg (and its original ratio), Reock,
13
+ rectangularity, elongation, aspect ratio, area and perimeter convexity, FRAGSTATS
14
+ fractal dimension; MBR orientation as angle and azimuth.
15
+ - Frontage against street centrelines or block outlines (exact capsule intervals with an
16
+ angle filter), per-street lengths, corner / through / interior / landlocked lot types.
17
+ - Lot depth, width, mean depth and width-to-depth ratio relative to the primary front.
18
+ - Buildable envelope by uniform, per-edge or role-based setbacks (exact for convex
19
+ parcels; verified mitred offset with edge-collapse handling for non-convex parcels).
20
+ - Lot rules (minimum area, frontage, depth, width) and subdivision feasibility by
21
+ equal-frontage or equal-area perpendicular cuts.
22
+ - FeatureCollection batch analysis, envelope and subdivision layers; GeoJSON and CSV I/O.
23
+ - Command line: `parcelform metrics | envelope | subdivide`.
24
+
25
+ ### Fixed (pre-release review)
26
+ - Frontage capsule intervals: an edge that passes a street segment's end inside the end
27
+ disc, but crosses the perpendicular band only beyond the end, no longer gets an
28
+ over-long frontage interval; checked against dense sampling in the tests.
29
+ - Shape-index docs: the two convexity ratios are labelled as the classical measures
30
+ discussed by Zunic & Rosin (2004); reference details completed.
@@ -0,0 +1,23 @@
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: "geospacephilo@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "parcelform"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-15"
11
+ license: "MIT"
12
+ repository-code: "https://gitlab.com/geophilo1/parcelform_sdk"
13
+ url: "https://pypi.org/project/parcelform/"
14
+ abstract: "Parcel and lot geometry metrics from GeoJSON polygons in projected coordinates: area, perimeter, convex hull, minimum-area rectangle and orientation; Polsby-Popper, Schwartzberg, Reock, rectangularity, elongation, convexity and fractal dimension; frontage against street centrelines or blocks with corner, through and interior lot classification; lot depth and width; buildable envelopes by setback; lot-rule checks and subdivision feasibility."
15
+ keywords:
16
+ - parcel
17
+ - lot
18
+ - cadastre
19
+ - frontage
20
+ - setback
21
+ - shape index
22
+ - urban planning
23
+ - gis
@@ -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,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/parcelform/py.typed
7
+ recursive-include src/parcelform *.py
8
+ recursive-include tests *.py
9
+ recursive-include examples *.py
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: parcelform
3
+ Version: 0.1.0
4
+ Summary: Parcel and lot geometry metrics from GeoJSON polygons: shape indices, minimum-area rectangle, frontage and lot type, depth and width, setback envelopes and subdivision feasibility checks.
5
+ Author-email: Yusuf Eminoğlu <geospacephilo@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/geophilo1/parcelform_sdk
8
+ Project-URL: Repository, https://gitlab.com/geophilo1/parcelform_sdk
9
+ Project-URL: Issues, https://gitlab.com/geophilo1/parcelform_sdk/-/work_items
10
+ Project-URL: Changelog, https://gitlab.com/geophilo1/parcelform_sdk/-/blob/main/CHANGELOG.md
11
+ Keywords: parcel,lot,cadastre,land-subdivision,frontage,setback,zoning,shape-index,polsby-popper,minimum-bounding-rectangle,geojson,urban-planning,gis
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: Environment :: Console
21
+ Classifier: Topic :: Scientific/Engineering :: GIS
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.21
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # parcelform
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/parcelform.svg)](https://pypi.org/project/parcelform/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/parcelform.svg)](https://pypi.org/project/parcelform/)
40
+ [![pipeline](https://gitlab.com/geophilo1/parcelform_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/parcelform_sdk/-/pipelines)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/parcelform_sdk/-/blob/main/LICENSE)
42
+
43
+ Parcel and lot geometry metrics from GeoJSON polygons in projected coordinates.
44
+
45
+ `parcelform` measures cadastral parcels and building lots the way planners and land-use
46
+ analysts usually need them: size and shape, how much of the lot faces a street, whether it
47
+ is a corner, through or interior lot, how deep and wide it is, what is left after
48
+ setbacks, and whether it can be split into lots that meet minimum standards. It needs only
49
+ NumPy and reads and writes plain GeoJSON.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install parcelform
55
+ ```
56
+
57
+ Requires Python 3.9 or newer and NumPy. Coordinates must be projected (metres or feet);
58
+ the package does not reproject and refuses data that looks like longitude/latitude.
59
+
60
+ ## Quickstart
61
+
62
+ ```python
63
+ import parcelform as pf
64
+
65
+ lot = {"type": "Polygon",
66
+ "coordinates": [[[0, 0], [20, 0], [20, 40], [0, 40], [0, 0]]]}
67
+ streets = [{"type": "LineString", "coordinates": [[-50, -4], [200, -4]]}, # main street
68
+ {"type": "LineString", "coordinates": [[-4, -50], [-4, 100]]}] # side street
69
+
70
+ pf.shape_indices(lot).polsby_popper # 0.698
71
+ pf.min_area_rectangle(lot).to_dict() # length 40, width 20, angle 90
72
+
73
+ fr = pf.frontage(lot, streets, tolerance=5, primary_rule="shortest")
74
+ fr.total, fr.lot_type # 60.0, 'corner'
75
+ pf.lot_dimensions(lot, fr) # depth 40, width 20, width/depth 0.5
76
+
77
+ env = pf.buildable_envelope(lot, {"front": 5, "side": 3, "rear": 4,
78
+ "secondary_front": 3}, fr)
79
+ env.area, env.share # 434.0, 0.5425
80
+
81
+ rules = pf.LotRules(min_area=300, min_frontage=12, min_depth=20) # from your zoning code
82
+ pf.check_lot(lot, rules, streets, tolerance=5).passed
83
+
84
+ wide = {"type": "Polygon",
85
+ "coordinates": [[[0, 0], [60, 0], [60, 30], [0, 30], [0, 0]]]}
86
+ res = pf.subdivision_feasibility(wide, rules, streets[:1], tolerance=4)
87
+ res.n_lots # 5 lots of 12 m x 30 m
88
+ ```
89
+
90
+ Whole layers in one call:
91
+
92
+ ```python
93
+ fc = pf.analyze("parcels.geojson", streets="streets.geojson", tolerance=8,
94
+ setbacks={"front": 5, "side": 3, "rear": 3},
95
+ rules=pf.LotRules(min_area=400, min_frontage=12), subdivide=True,
96
+ on_error="record")
97
+ pf.write_geojson(fc, "parcels_measured.geojson")
98
+ ```
99
+
100
+ ## Features
101
+
102
+ | Area | Functions | Notes |
103
+ | --- | --- | --- |
104
+ | Geometry | `area`, `perimeter`, `centroid`, `convex_hull`, `min_area_rectangle`, `min_enclosing_circle`, `points_in_polygon` | holes and MultiPolygons supported |
105
+ | Orientation | `Rectangle.angle`, `Rectangle.azimuth`, `orientation_difference` | long axis of the minimum-area rectangle |
106
+ | Shape indices | `shape_indices`, `polsby_popper`, `schwartzberg`, `fractal_dimension` | Polsby-Popper, Schwartzberg, Reock, rectangularity, elongation, aspect ratio, area and perimeter convexity, fractal dimension |
107
+ | Frontage | `frontage -> Frontage`, `StreetSegments`, `as_streets` | street centrelines or block outlines; per-street lengths; corner / through / interior / landlocked |
108
+ | Lot dimensions | `lot_dimensions -> LotDimensions` | depth, width, mean depth, width-to-depth ratio |
109
+ | Setbacks | `buildable_envelope -> Envelope`, `edge_roles`, `resolve_setbacks` | uniform, per edge, or by role (front, side, rear, secondary front) |
110
+ | Lot rules | `LotRules`, `check_lot`, `evaluate_rules` | minimum area, frontage, depth, width (all user-supplied) |
111
+ | Subdivision | `subdivision_feasibility`, `split_parcel`, `lot_count_bound` | equal-frontage or equal-area perpendicular cuts |
112
+ | Layers | `analyze`, `envelopes`, `subdivide_collection` | FeatureCollection in, FeatureCollection out |
113
+ | I/O | `read_geojson`, `write_geojson`, `write_csv`, `read_rules` | standard library only |
114
+
115
+ ## Methods
116
+
117
+ ### Geometry
118
+
119
+ - Areas by the shoelace formula; perimeters include holes unless `include_holes=False`.
120
+ - Convex hull by Andrew's (1979) monotone chain.
121
+ - **Minimum-area rectangle** by rotating calipers: one side of the optimal rectangle is
122
+ collinear with a convex-hull edge (Freeman & Shapira 1975; Toussaint 1983), so every
123
+ hull-edge direction is tried, vectorised. `length` is the long side, `width` the short
124
+ side, `angle` the long axis counter-clockwise from +x in `[0, 180)`, `azimuth` the same
125
+ axis as a bearing from grid north.
126
+ - Minimum enclosing circle by Welzl's (1991) algorithm with a fixed shuffle seed.
127
+
128
+ ### Shape indices
129
+
130
+ `A` area, `P` perimeter, `A_h`/`P_h` convex hull area and perimeter, `L`/`W` long and short
131
+ side of the minimum-area rectangle (MBR).
132
+
133
+ | Index | Formula | Range | Source |
134
+ | --- | --- | --- | --- |
135
+ | Polsby-Popper | `4 pi A / P^2` | (0, 1], circle = 1 | Polsby & Popper (1991) |
136
+ | Schwartzberg | `2 sqrt(pi A) / P` | (0, 1], circle = 1 | Schwartzberg (1966) |
137
+ | Reock | `A / A_circle` (minimum enclosing circle) | (0, 1] | Reock (1961) |
138
+ | Rectangularity | `A / (L W)` | (0, 1] | Rosin (1999) |
139
+ | Elongation | `1 - W / L` | [0, 1), square MBR = 0 | MBR (Freeman & Shapira 1975) |
140
+ | Aspect ratio | `L / W` | >= 1 | MBR (Freeman & Shapira 1975) |
141
+ | Convexity (area, "solidity") | `A / A_h` | (0, 1] | classical measure, see Zunic & Rosin (2004) |
142
+ | Convexity (perimeter) | `P_h / P` | (0, 1] | classical measure, see Zunic & Rosin (2004) |
143
+ | Fractal dimension | `2 ln(0.25 P) / ln A` | about 1-2 | McGarigal & Marks (1995), FRAGSTATS FRAC |
144
+
145
+ Schwartzberg (1966) defined `P / (2 sqrt(pi A))` (>= 1); `schwartzberg` reports its
146
+ reciprocal so that all compactness indices point the same way, and `schwartzberg_ratio`
147
+ gives the original. The fractal dimension uses the FRAGSTATS correction (a square scores
148
+ 1); it depends on the length unit and is NaN for `A <= 1`.
149
+
150
+ ### Frontage and lot type
151
+
152
+ For every parcel edge and street segment, the part of the edge within `tolerance` of the
153
+ segment is the intersection of the edge with the segment's capsule (a convex set, so the
154
+ answer is one interval, computed in closed form). Only parts where the edge runs within
155
+ `max_angle` (default 20 degrees) of the street direction count, so side lot lines that end
156
+ at the street do not. Intervals are merged per edge, so overlapping street lines are not
157
+ double-counted; `by_street` gives the length per street id.
158
+
159
+ With street **centrelines**, set `tolerance` to about half the right-of-way width plus a
160
+ digitising allowance. With **block outlines** (or lines on the street side of the lots),
161
+ use a small tolerance.
162
+
163
+ - `corner`: two frontage pieces at `corner_angle` (default 45 degrees) or more to each
164
+ other;
165
+ - `through`: two roughly parallel frontage pieces facing away from each other;
166
+ - `interior`: frontage on one side; `landlocked`: none.
167
+
168
+ The **primary front** is the longest frontage piece by default; `primary_rule="shortest"`
169
+ uses the shortest piece, which matches codes that put the front of a corner lot on the
170
+ narrower street side. `depth` is the extent of the lot perpendicular to the primary front,
171
+ `width` the extent along it, `mean_depth = area / width`. Without frontage the MBR is used
172
+ (`basis = "mbr"`). Zoning codes define lot depth and width in several ways; check that
173
+ these definitions fit the code you apply.
174
+
175
+ ### Buildable envelope
176
+
177
+ Each lot line is moved inward, parallel to itself, by its setback, and the envelope is the
178
+ area inside all setback lines. Setbacks are uniform, per edge, or by role: an edge is a
179
+ `front` edge when at least half of it is frontage, `secondary_front` when it is a front
180
+ edge turned 45 degrees or more from the primary front, `rear` when it faces away from the
181
+ primary front, otherwise `side`.
182
+
183
+ - Convex parcels: exact half-plane intersection by Sutherland-Hodgman clipping
184
+ (Sutherland & Hodgman 1974).
185
+ - Non-convex parcels: mitred offset with removal of collapsing edges, as in the edge
186
+ events of a straight skeleton (Aichholzer et al. 1995), then verified (simple ring, every
187
+ vertex and edge midpoint keeps every setback). An envelope that would split into pieces
188
+ raises `ParcelError`; one with no buildable point is returned empty.
189
+
190
+ ### Subdivision feasibility
191
+
192
+ `subdivision_feasibility` first checks the parcel against the rules, bounds the lot count
193
+ by `floor(min(area / min_area, frontage / min_frontage))`, then tries that count and fewer,
194
+ splitting the parcel by straight cuts perpendicular to the primary front (equal frontage or
195
+ equal area) and re-measuring every new lot's frontage, depth and width against the
196
+ streets. It returns the largest conforming count (`0` if the parcel itself fails, `1` if it
197
+ cannot be split). It is a screening tool: flag lots, curved streets, easements and access
198
+ rules are out of scope.
199
+
200
+ No lot standards or setback distances are built in. They are parameters, because they come
201
+ from the zoning or subdivision code that applies.
202
+
203
+ ## Command line
204
+
205
+ ```bash
206
+ parcelform metrics parcels.geojson --streets streets.geojson --tolerance 8 \
207
+ --front 5 --side 3 --rear 3 --min-area 400 --min-frontage 12 --subdivide \
208
+ -o parcels_measured.geojson --csv parcels.csv
209
+
210
+ parcelform envelope parcels.geojson --setback 3 -o envelopes.geojson
211
+ parcelform envelope parcels.geojson --streets streets.geojson --tolerance 8 \
212
+ --front 5 --side 3 --rear 4 --secondary-front 3 -o envelopes.geojson
213
+
214
+ parcelform subdivide parcels.geojson --streets streets.geojson --tolerance 8 \
215
+ --rules rules.json --by area -o lots.geojson --csv lots.csv
216
+ ```
217
+
218
+ `--rules` takes a JSON object or a two-column `key,value` CSV with `min_area`,
219
+ `min_frontage`, `min_depth`, `min_width`. Without `-o` and `--csv`, GeoJSON goes to stdout.
220
+
221
+ ## Limitations
222
+
223
+ - Planar geometry only; no reprojection, no curved segments.
224
+ - Non-convex envelopes that would split into several pieces are not computed; split the
225
+ parcel first.
226
+ - Lot classification uses straight frontage pieces; along strongly curved streets
227
+ (cul-de-sac bulbs) set `min_piece` or `corner_angle` to avoid false corner lots.
228
+ - Subdivision uses parallel straight cuts only.
229
+
230
+ ## References
231
+
232
+ - Aichholzer, O., Aurenhammer, F., Alberts, D. & Gärtner, B. (1995). A novel type of
233
+ skeleton for polygons. *Journal of Universal Computer Science* 1(12), 752-761.
234
+ - Andrew, A. M. (1979). Another efficient algorithm for convex hulls in two dimensions.
235
+ *Information Processing Letters* 9(5), 216-219.
236
+ - Freeman, H. & Shapira, R. (1975). Determining the minimum-area encasing rectangle for an
237
+ arbitrary closed curve. *Communications of the ACM* 18(7), 409-413.
238
+ - McGarigal, K. & Marks, B. J. (1995). FRAGSTATS: spatial pattern analysis program for
239
+ quantifying landscape structure. USDA Forest Service, Gen. Tech. Rep. PNW-GTR-351.
240
+ - Polsby, D. D. & Popper, R. D. (1991). The third criterion: compactness as a procedural
241
+ safeguard against partisan gerrymandering. *Yale Law & Policy Review* 9(2), 301-353.
242
+ - Reock, E. C. (1961). A note: measuring compactness as a requirement of legislative
243
+ apportionment. *Midwest Journal of Political Science* 5(1), 70-74.
244
+ - Rosin, P. L. (1999). Measuring rectangularity. *Machine Vision and Applications* 11(4),
245
+ 191-196.
246
+ - Schwartzberg, J. E. (1966). Reapportionment, gerrymanders, and the notion of
247
+ compactness. *Minnesota Law Review* 50, 443-452.
248
+ - Sutherland, I. E. & Hodgman, G. W. (1974). Reentrant polygon clipping. *Communications
249
+ of the ACM* 17(1), 32-42.
250
+ - Toussaint, G. T. (1983). Solving geometric problems with the rotating calipers.
251
+ *Proceedings of IEEE MELECON '83*, Athens.
252
+ - Welzl, E. (1991). Smallest enclosing disks (balls and ellipsoids). In *New Results and
253
+ New Trends in Computer Science*, LNCS 555, 359-370.
254
+ - Zunic, J. & Rosin, P. L. (2004). A new convexity measure for polygons. *IEEE
255
+ Transactions on Pattern Analysis and Machine Intelligence* 26(7), 923-934.
256
+
257
+ ## Citation
258
+
259
+ ```bibtex
260
+ @software{eminoglu_parcelform,
261
+ author = {Eminoğlu, Yusuf},
262
+ title = {parcelform},
263
+ year = {2026},
264
+ version = {0.1.0},
265
+ url = {https://gitlab.com/geophilo1/parcelform_sdk}
266
+ }
267
+ ```
268
+
269
+ See also [CITATION.cff](CITATION.cff).
270
+
271
+ ## License
272
+
273
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,238 @@
1
+ # parcelform
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/parcelform.svg)](https://pypi.org/project/parcelform/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/parcelform.svg)](https://pypi.org/project/parcelform/)
5
+ [![pipeline](https://gitlab.com/geophilo1/parcelform_sdk/badges/main/pipeline.svg)](https://gitlab.com/geophilo1/parcelform_sdk/-/pipelines)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitlab.com/geophilo1/parcelform_sdk/-/blob/main/LICENSE)
7
+
8
+ Parcel and lot geometry metrics from GeoJSON polygons in projected coordinates.
9
+
10
+ `parcelform` measures cadastral parcels and building lots the way planners and land-use
11
+ analysts usually need them: size and shape, how much of the lot faces a street, whether it
12
+ is a corner, through or interior lot, how deep and wide it is, what is left after
13
+ setbacks, and whether it can be split into lots that meet minimum standards. It needs only
14
+ NumPy and reads and writes plain GeoJSON.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install parcelform
20
+ ```
21
+
22
+ Requires Python 3.9 or newer and NumPy. Coordinates must be projected (metres or feet);
23
+ the package does not reproject and refuses data that looks like longitude/latitude.
24
+
25
+ ## Quickstart
26
+
27
+ ```python
28
+ import parcelform as pf
29
+
30
+ lot = {"type": "Polygon",
31
+ "coordinates": [[[0, 0], [20, 0], [20, 40], [0, 40], [0, 0]]]}
32
+ streets = [{"type": "LineString", "coordinates": [[-50, -4], [200, -4]]}, # main street
33
+ {"type": "LineString", "coordinates": [[-4, -50], [-4, 100]]}] # side street
34
+
35
+ pf.shape_indices(lot).polsby_popper # 0.698
36
+ pf.min_area_rectangle(lot).to_dict() # length 40, width 20, angle 90
37
+
38
+ fr = pf.frontage(lot, streets, tolerance=5, primary_rule="shortest")
39
+ fr.total, fr.lot_type # 60.0, 'corner'
40
+ pf.lot_dimensions(lot, fr) # depth 40, width 20, width/depth 0.5
41
+
42
+ env = pf.buildable_envelope(lot, {"front": 5, "side": 3, "rear": 4,
43
+ "secondary_front": 3}, fr)
44
+ env.area, env.share # 434.0, 0.5425
45
+
46
+ rules = pf.LotRules(min_area=300, min_frontage=12, min_depth=20) # from your zoning code
47
+ pf.check_lot(lot, rules, streets, tolerance=5).passed
48
+
49
+ wide = {"type": "Polygon",
50
+ "coordinates": [[[0, 0], [60, 0], [60, 30], [0, 30], [0, 0]]]}
51
+ res = pf.subdivision_feasibility(wide, rules, streets[:1], tolerance=4)
52
+ res.n_lots # 5 lots of 12 m x 30 m
53
+ ```
54
+
55
+ Whole layers in one call:
56
+
57
+ ```python
58
+ fc = pf.analyze("parcels.geojson", streets="streets.geojson", tolerance=8,
59
+ setbacks={"front": 5, "side": 3, "rear": 3},
60
+ rules=pf.LotRules(min_area=400, min_frontage=12), subdivide=True,
61
+ on_error="record")
62
+ pf.write_geojson(fc, "parcels_measured.geojson")
63
+ ```
64
+
65
+ ## Features
66
+
67
+ | Area | Functions | Notes |
68
+ | --- | --- | --- |
69
+ | Geometry | `area`, `perimeter`, `centroid`, `convex_hull`, `min_area_rectangle`, `min_enclosing_circle`, `points_in_polygon` | holes and MultiPolygons supported |
70
+ | Orientation | `Rectangle.angle`, `Rectangle.azimuth`, `orientation_difference` | long axis of the minimum-area rectangle |
71
+ | Shape indices | `shape_indices`, `polsby_popper`, `schwartzberg`, `fractal_dimension` | Polsby-Popper, Schwartzberg, Reock, rectangularity, elongation, aspect ratio, area and perimeter convexity, fractal dimension |
72
+ | Frontage | `frontage -> Frontage`, `StreetSegments`, `as_streets` | street centrelines or block outlines; per-street lengths; corner / through / interior / landlocked |
73
+ | Lot dimensions | `lot_dimensions -> LotDimensions` | depth, width, mean depth, width-to-depth ratio |
74
+ | Setbacks | `buildable_envelope -> Envelope`, `edge_roles`, `resolve_setbacks` | uniform, per edge, or by role (front, side, rear, secondary front) |
75
+ | Lot rules | `LotRules`, `check_lot`, `evaluate_rules` | minimum area, frontage, depth, width (all user-supplied) |
76
+ | Subdivision | `subdivision_feasibility`, `split_parcel`, `lot_count_bound` | equal-frontage or equal-area perpendicular cuts |
77
+ | Layers | `analyze`, `envelopes`, `subdivide_collection` | FeatureCollection in, FeatureCollection out |
78
+ | I/O | `read_geojson`, `write_geojson`, `write_csv`, `read_rules` | standard library only |
79
+
80
+ ## Methods
81
+
82
+ ### Geometry
83
+
84
+ - Areas by the shoelace formula; perimeters include holes unless `include_holes=False`.
85
+ - Convex hull by Andrew's (1979) monotone chain.
86
+ - **Minimum-area rectangle** by rotating calipers: one side of the optimal rectangle is
87
+ collinear with a convex-hull edge (Freeman & Shapira 1975; Toussaint 1983), so every
88
+ hull-edge direction is tried, vectorised. `length` is the long side, `width` the short
89
+ side, `angle` the long axis counter-clockwise from +x in `[0, 180)`, `azimuth` the same
90
+ axis as a bearing from grid north.
91
+ - Minimum enclosing circle by Welzl's (1991) algorithm with a fixed shuffle seed.
92
+
93
+ ### Shape indices
94
+
95
+ `A` area, `P` perimeter, `A_h`/`P_h` convex hull area and perimeter, `L`/`W` long and short
96
+ side of the minimum-area rectangle (MBR).
97
+
98
+ | Index | Formula | Range | Source |
99
+ | --- | --- | --- | --- |
100
+ | Polsby-Popper | `4 pi A / P^2` | (0, 1], circle = 1 | Polsby & Popper (1991) |
101
+ | Schwartzberg | `2 sqrt(pi A) / P` | (0, 1], circle = 1 | Schwartzberg (1966) |
102
+ | Reock | `A / A_circle` (minimum enclosing circle) | (0, 1] | Reock (1961) |
103
+ | Rectangularity | `A / (L W)` | (0, 1] | Rosin (1999) |
104
+ | Elongation | `1 - W / L` | [0, 1), square MBR = 0 | MBR (Freeman & Shapira 1975) |
105
+ | Aspect ratio | `L / W` | >= 1 | MBR (Freeman & Shapira 1975) |
106
+ | Convexity (area, "solidity") | `A / A_h` | (0, 1] | classical measure, see Zunic & Rosin (2004) |
107
+ | Convexity (perimeter) | `P_h / P` | (0, 1] | classical measure, see Zunic & Rosin (2004) |
108
+ | Fractal dimension | `2 ln(0.25 P) / ln A` | about 1-2 | McGarigal & Marks (1995), FRAGSTATS FRAC |
109
+
110
+ Schwartzberg (1966) defined `P / (2 sqrt(pi A))` (>= 1); `schwartzberg` reports its
111
+ reciprocal so that all compactness indices point the same way, and `schwartzberg_ratio`
112
+ gives the original. The fractal dimension uses the FRAGSTATS correction (a square scores
113
+ 1); it depends on the length unit and is NaN for `A <= 1`.
114
+
115
+ ### Frontage and lot type
116
+
117
+ For every parcel edge and street segment, the part of the edge within `tolerance` of the
118
+ segment is the intersection of the edge with the segment's capsule (a convex set, so the
119
+ answer is one interval, computed in closed form). Only parts where the edge runs within
120
+ `max_angle` (default 20 degrees) of the street direction count, so side lot lines that end
121
+ at the street do not. Intervals are merged per edge, so overlapping street lines are not
122
+ double-counted; `by_street` gives the length per street id.
123
+
124
+ With street **centrelines**, set `tolerance` to about half the right-of-way width plus a
125
+ digitising allowance. With **block outlines** (or lines on the street side of the lots),
126
+ use a small tolerance.
127
+
128
+ - `corner`: two frontage pieces at `corner_angle` (default 45 degrees) or more to each
129
+ other;
130
+ - `through`: two roughly parallel frontage pieces facing away from each other;
131
+ - `interior`: frontage on one side; `landlocked`: none.
132
+
133
+ The **primary front** is the longest frontage piece by default; `primary_rule="shortest"`
134
+ uses the shortest piece, which matches codes that put the front of a corner lot on the
135
+ narrower street side. `depth` is the extent of the lot perpendicular to the primary front,
136
+ `width` the extent along it, `mean_depth = area / width`. Without frontage the MBR is used
137
+ (`basis = "mbr"`). Zoning codes define lot depth and width in several ways; check that
138
+ these definitions fit the code you apply.
139
+
140
+ ### Buildable envelope
141
+
142
+ Each lot line is moved inward, parallel to itself, by its setback, and the envelope is the
143
+ area inside all setback lines. Setbacks are uniform, per edge, or by role: an edge is a
144
+ `front` edge when at least half of it is frontage, `secondary_front` when it is a front
145
+ edge turned 45 degrees or more from the primary front, `rear` when it faces away from the
146
+ primary front, otherwise `side`.
147
+
148
+ - Convex parcels: exact half-plane intersection by Sutherland-Hodgman clipping
149
+ (Sutherland & Hodgman 1974).
150
+ - Non-convex parcels: mitred offset with removal of collapsing edges, as in the edge
151
+ events of a straight skeleton (Aichholzer et al. 1995), then verified (simple ring, every
152
+ vertex and edge midpoint keeps every setback). An envelope that would split into pieces
153
+ raises `ParcelError`; one with no buildable point is returned empty.
154
+
155
+ ### Subdivision feasibility
156
+
157
+ `subdivision_feasibility` first checks the parcel against the rules, bounds the lot count
158
+ by `floor(min(area / min_area, frontage / min_frontage))`, then tries that count and fewer,
159
+ splitting the parcel by straight cuts perpendicular to the primary front (equal frontage or
160
+ equal area) and re-measuring every new lot's frontage, depth and width against the
161
+ streets. It returns the largest conforming count (`0` if the parcel itself fails, `1` if it
162
+ cannot be split). It is a screening tool: flag lots, curved streets, easements and access
163
+ rules are out of scope.
164
+
165
+ No lot standards or setback distances are built in. They are parameters, because they come
166
+ from the zoning or subdivision code that applies.
167
+
168
+ ## Command line
169
+
170
+ ```bash
171
+ parcelform metrics parcels.geojson --streets streets.geojson --tolerance 8 \
172
+ --front 5 --side 3 --rear 3 --min-area 400 --min-frontage 12 --subdivide \
173
+ -o parcels_measured.geojson --csv parcels.csv
174
+
175
+ parcelform envelope parcels.geojson --setback 3 -o envelopes.geojson
176
+ parcelform envelope parcels.geojson --streets streets.geojson --tolerance 8 \
177
+ --front 5 --side 3 --rear 4 --secondary-front 3 -o envelopes.geojson
178
+
179
+ parcelform subdivide parcels.geojson --streets streets.geojson --tolerance 8 \
180
+ --rules rules.json --by area -o lots.geojson --csv lots.csv
181
+ ```
182
+
183
+ `--rules` takes a JSON object or a two-column `key,value` CSV with `min_area`,
184
+ `min_frontage`, `min_depth`, `min_width`. Without `-o` and `--csv`, GeoJSON goes to stdout.
185
+
186
+ ## Limitations
187
+
188
+ - Planar geometry only; no reprojection, no curved segments.
189
+ - Non-convex envelopes that would split into several pieces are not computed; split the
190
+ parcel first.
191
+ - Lot classification uses straight frontage pieces; along strongly curved streets
192
+ (cul-de-sac bulbs) set `min_piece` or `corner_angle` to avoid false corner lots.
193
+ - Subdivision uses parallel straight cuts only.
194
+
195
+ ## References
196
+
197
+ - Aichholzer, O., Aurenhammer, F., Alberts, D. & Gärtner, B. (1995). A novel type of
198
+ skeleton for polygons. *Journal of Universal Computer Science* 1(12), 752-761.
199
+ - Andrew, A. M. (1979). Another efficient algorithm for convex hulls in two dimensions.
200
+ *Information Processing Letters* 9(5), 216-219.
201
+ - Freeman, H. & Shapira, R. (1975). Determining the minimum-area encasing rectangle for an
202
+ arbitrary closed curve. *Communications of the ACM* 18(7), 409-413.
203
+ - McGarigal, K. & Marks, B. J. (1995). FRAGSTATS: spatial pattern analysis program for
204
+ quantifying landscape structure. USDA Forest Service, Gen. Tech. Rep. PNW-GTR-351.
205
+ - Polsby, D. D. & Popper, R. D. (1991). The third criterion: compactness as a procedural
206
+ safeguard against partisan gerrymandering. *Yale Law & Policy Review* 9(2), 301-353.
207
+ - Reock, E. C. (1961). A note: measuring compactness as a requirement of legislative
208
+ apportionment. *Midwest Journal of Political Science* 5(1), 70-74.
209
+ - Rosin, P. L. (1999). Measuring rectangularity. *Machine Vision and Applications* 11(4),
210
+ 191-196.
211
+ - Schwartzberg, J. E. (1966). Reapportionment, gerrymanders, and the notion of
212
+ compactness. *Minnesota Law Review* 50, 443-452.
213
+ - Sutherland, I. E. & Hodgman, G. W. (1974). Reentrant polygon clipping. *Communications
214
+ of the ACM* 17(1), 32-42.
215
+ - Toussaint, G. T. (1983). Solving geometric problems with the rotating calipers.
216
+ *Proceedings of IEEE MELECON '83*, Athens.
217
+ - Welzl, E. (1991). Smallest enclosing disks (balls and ellipsoids). In *New Results and
218
+ New Trends in Computer Science*, LNCS 555, 359-370.
219
+ - Zunic, J. & Rosin, P. L. (2004). A new convexity measure for polygons. *IEEE
220
+ Transactions on Pattern Analysis and Machine Intelligence* 26(7), 923-934.
221
+
222
+ ## Citation
223
+
224
+ ```bibtex
225
+ @software{eminoglu_parcelform,
226
+ author = {Eminoğlu, Yusuf},
227
+ title = {parcelform},
228
+ year = {2026},
229
+ version = {0.1.0},
230
+ url = {https://gitlab.com/geophilo1/parcelform_sdk}
231
+ }
232
+ ```
233
+
234
+ See also [CITATION.cff](CITATION.cff).
235
+
236
+ ## License
237
+
238
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,65 @@
1
+ """parcelform quickstart on a small synthetic block (no input files needed).
2
+
3
+ Run: python examples/quickstart.py
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ import parcelform as pf
11
+
12
+
13
+ def square(x0: float, y0: float, w: float, d: float) -> dict:
14
+ ring = [[x0, y0], [x0 + w, y0], [x0 + w, y0 + d], [x0, y0 + d], [x0, y0]]
15
+ return {"type": "Polygon", "coordinates": [ring]}
16
+
17
+
18
+ def main() -> None:
19
+ # Three lots along a street whose centreline runs 6 m south of the lots; the
20
+ # westernmost lot also faces a side street.
21
+ parcels = {
22
+ "type": "FeatureCollection",
23
+ "features": [
24
+ {"type": "Feature", "id": "A", "properties": {}, "geometry": square(0, 0, 18, 35)},
25
+ {"type": "Feature", "id": "B", "properties": {}, "geometry": square(18, 0, 60, 35)},
26
+ {"type": "Feature", "id": "C", "properties": {}, "geometry": square(78, 10, 15, 25)},
27
+ ],
28
+ }
29
+ streets = [
30
+ {"type": "LineString", "coordinates": [[-50, -6], [150, -6]]},
31
+ {"type": "LineString", "coordinates": [[-6, -50], [-6, 100]]},
32
+ ]
33
+
34
+ rep = pf.analyze_parcel(parcels["features"][0]["geometry"], streets=streets, tolerance=7)
35
+ print("lot A:", rep.frontage.lot_type, "frontage", rep.frontage.total)
36
+ print("shape:", {k: round(v, 3) for k, v in rep.shape.to_dict().items()})
37
+
38
+ # Zoning values below are placeholders; take them from the code that applies.
39
+ rules = pf.LotRules(min_area=400, min_frontage=12, min_depth=25)
40
+ out = pf.analyze(
41
+ parcels,
42
+ streets=streets,
43
+ tolerance=7,
44
+ setbacks={"front": 5, "side": 3, "rear": 3},
45
+ rules=rules,
46
+ subdivide=True,
47
+ primary_rule="shortest", # corner lots front on their narrower street side
48
+ )
49
+ for f in out["features"]:
50
+ p = f["properties"]
51
+ print(
52
+ f["id"],
53
+ p.get("lot_type"),
54
+ "frontage",
55
+ round(p.get("frontage", 0), 2),
56
+ "envelope",
57
+ round(p.get("envelope_area") or 0, 1),
58
+ "max lots",
59
+ p.get("max_lots"),
60
+ )
61
+ print(json.dumps(out["features"][1]["properties"], indent=1)[:400])
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()