t3grid 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,495 @@
1
+ Metadata-Version: 2.4
2
+ Name: t3grid
3
+ Version: 0.1.0
4
+ Summary: Trifold T3: hierarchical triangular DGGS on the icosahedron with exact aperture-4 nesting and compact base32 addressing
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://jaakla.github.io/trifold/
7
+ Project-URL: Repository, https://github.com/jaakla/trifold
8
+ Project-URL: Documentation, https://github.com/jaakla/trifold/blob/main/docs/t3-technical-reference.md
9
+ Keywords: dggs,geospatial,triangular-grid,global-grid,icosahedron,h3-alternative,spatial-index
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: GIS
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy
23
+ Provides-Extra: land
24
+ Requires-Dist: shapely>=2.0; extra == "land"
25
+ Requires-Dist: pyproj; extra == "land"
26
+ Provides-Extra: build
27
+ Requires-Dist: shapely>=2.0; extra == "build"
28
+ Requires-Dist: pyproj; extra == "build"
29
+ Requires-Dist: geopandas; extra == "build"
30
+ Requires-Dist: topojson; extra == "build"
31
+ Requires-Dist: h3>=4; extra == "build"
32
+ Requires-Dist: pya5; extra == "build"
33
+ Requires-Dist: s2sphere; extra == "build"
34
+ Requires-Dist: rhealpixdggs; extra == "build"
35
+ Requires-Dist: matplotlib; extra == "build"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # Trifold T3 — a hierarchical triangular DGGS with *exact* nesting
41
+
42
+ **Triangles tile the sphere into a quadtree where every parent is *exactly*
43
+ the union of its four children — something neither hexagons nor most
44
+ square systems can offer — with a 6-character address for a ~110 km cell.**
45
+
46
+ **[Live demo & intro site](https://jaakla.github.io/trifold/)** · globe ↔ flat ·
47
+ 7 grid systems side by side · click any cell for its address ·
48
+ [![Technical reference](https://img.shields.io/badge/Technical%20Reference-Docs-blue?logo=github)](https://github.com/jaakla/trifold/blob/main/docs/t3-technical-reference.md)
49
+
50
+ ![global overview](docs/img/global_overview.png)
51
+
52
+ ---
53
+
54
+ ## 1. The idea in 30 seconds
55
+
56
+ Start from the icosahedron: 20 spherical triangles covering the Earth.
57
+ Split every triangle into 4 by connecting the great-circle midpoints of its
58
+ edges. Repeat. Each level halves the edge length and quadruples the cell
59
+ count (*aperture 4*):
60
+
61
+ | level | mean edge | mean area | cells (global) |
62
+ |---:|---:|---:|---:|
63
+ | 0 | 7,054 km | 25.5M km² | 20 |
64
+ | 3 | 882 km | 399k km² | 1,280 |
65
+ | 6 | 110 km | 6,226 km² | 81,920 |
66
+ | 9 | 13.8 km | 97 km² | 5.2M |
67
+ | 12 | 1.7 km | 1.5 km² | 336M |
68
+ | 15 | 215 m | 24 ha | 21.5B |
69
+
70
+ Because children are built from the parent's own vertices plus edge
71
+ midpoints, **a parent cell is bit-for-bit the union of its children**.
72
+ Aggregating data up the hierarchy or drilling down loses nothing and
73
+ double-counts nothing. That property — *exact nesting* (with limited size variation, about ±20%) — is the central property of this project and is uncommon among global grids (see [§6](#6-comparison-with-other-dggs)).
74
+
75
+ The repository contains the Python library, a three-form addressing codec,
76
+ global grid products generated against Natural Earth land, generators for
77
+ comparison grid systems, an interactive MapLibre demo (globe and flat),
78
+ and a Cloudflare Worker that computes cells on demand from the grid geometry.
79
+
80
+ ### SDK and application code
81
+
82
+ Core grid behavior is exposed through two standalone SDKs:
83
+
84
+ | Runtime | Public SDK | Code using it |
85
+ |---|---|---|
86
+ | Python | `trifold.api` (also re-exported by `trifold`) | CLI and build scripts |
87
+ | JavaScript | `js/trifold.js` / `@trifold/grid` | Cloudflare Worker and website |
88
+
89
+ The SDKs cover address codecs, hierarchy operations, point location, cell
90
+ geometry, metrics, and GeoJSON. Python land classification is an optional
91
+ extension under `trifold.land`. See the [SDK API reference](docs/sdk-api.md)
92
+ for the supported functions and examples.
93
+
94
+ ---
95
+
96
+ ## 2. Addressing: one identity, three encodings
97
+
98
+ A cell is identified by `(face, path)`: which of the 20 icosahedron faces
99
+ it lives on, and the sequence of base-4 digits choosing a child at every
100
+ subdivision (`0,1,2` = corner children toward the parent's vertices,
101
+ `3` = the central, orientation-flipped child).
102
+
103
+ The same identity has three interchangeable encodings, each optimized for
104
+ a different consumer:
105
+
106
+ | form | example (London, level 6) | for | size |
107
+ |---|---|---|---|
108
+ | **compact** | `TF6958` | humans, URLs, labels, CSV columns | 3 + ⌈2L/5⌉ chars |
109
+ | **path** | `F15-102111` | teaching, debugging — shows the tree descent | 4 + L chars |
110
+ | **addr64** | `8811996358392152070` | compute — sort, join, mask | 8 bytes |
111
+
112
+ **Why not just digits 0–3?** A digit string spends 8 bits per character to
113
+ carry 2 bits of information. The compact form re-encodes the *same* path
114
+ bits in Crockford base32 (5 bits/char, no ambiguous `I L O U`), prefixed
115
+ by face and level characters: `T` `F`(face 15) `6`(level 6) `958`(12 path
116
+ bits in 3 chars). Level 15 — sub-kilometre cells — still fits in 9
117
+ characters. Base64 would save little and is not URL-safe; raw binary is
118
+ not human-readable. Base32 provides a compact, URL-safe representation.
119
+
120
+ **The uint64 layout** packs face (5 bits) + up to 27 path digits (54 bits)
121
+ + level (5 bits). The path is left-aligned and the level is the low-bit
122
+ tie-breaker that places a parent before its descendants:
123
+
124
+ ```
125
+ 63 59 5 0
126
+ ┌─────────┬────────────────────────────────────────────────────────┬─────┐
127
+ │ face:5 │ path digits, 2 bits each, left-aligned │ L:5 │
128
+ └─────────┴────────────────────────────────────────────────────────┴─────┘
129
+ ```
130
+
131
+ Left-alignment and the low level field provide these properties:
132
+
133
+ * numeric sort = depth-first hierarchical order within a face
134
+ (Z-order curve — spatially adjacent cells tend to be numerically close);
135
+ * parent and child addresses are direct masks/shifts — no tree traversal;
136
+ * `is_ancestor(a, b)` = one shift and compare;
137
+ * `descendant_range(a)` returns the inclusive uint64 interval containing
138
+ that cell and all descendants, suitable for database range scans.
139
+
140
+ **Compatibility:** this corrected field order changes numeric `addr64`
141
+ values produced by the initial v0.1.0 code. Compact and path addresses are
142
+ unchanged; regenerate stored numeric IDs from either string form.
143
+
144
+ ```python
145
+ import trifold.api as tg
146
+ addr = tg.encode64(*tg.locate(-0.1276, 51.5072, level=6))
147
+ tg.to_compact(addr) # 'TF6958'
148
+ tg.to_path(addr) # 'F15-102111'
149
+ tg.to_compact(tg.parent64(addr)) # 'TF595'
150
+ [tg.to_compact(c) for c in tg.children64(addr)]
151
+ # ['TF7958', 'TF795A', 'TF795C', 'TF795E']
152
+ ```
153
+
154
+ Same answers from the command line (`pip install -e .`):
155
+
156
+ ```console
157
+ $ trifold locate -0.1276 51.5072 6
158
+ TF6958
159
+ $ trifold show TF6958
160
+ compact : TF6958
161
+ path : F15-102111
162
+ addr64 : 8811996358392152070 (0x7A4A800000000006)
163
+ level : 6
164
+ edge_km : 116.9
165
+ area_km2: 5864
166
+ $ trifold geom TF6958 > london_cell.geojson
167
+ ```
168
+
169
+ The same operations are available from the standalone
170
+ [JavaScript SDK](js/trifold.js) and the [Cloudflare Worker](worker/cell-server.js)
171
+ (`GET /locate/-0.1276,51.5072?level=6` → `TF6958`). The Worker is an HTTP
172
+ adapter over the SDK. The Python and JavaScript implementations are cross-tested.
173
+
174
+ ### Derived grouping keys
175
+
176
+ Every triangle can also be projected into two grouping indexes without
177
+ changing its geometry or accounting identity:
178
+
179
+ | property | role | behavior |
180
+ |---|---|---|
181
+ | `rhombus_id` | exact grouping | two triangles per rhombus on the complete grid |
182
+ | `rhombus_hilbert` | sort/partition key | Hilbert order within ten nested base diamonds |
183
+ | `hex_id` | display grouping | six triangles in face interiors; seam and vertex exceptions |
184
+
185
+ Rhombi have an exact aperture-4 hierarchy: a parent rhombus is the union of
186
+ four child rhombi. Hex groups are defined independently at each level and do
187
+ not nest. The face-local coloring produces three- or six-triangle seam groups
188
+ and fixed one- or five-triangle vertex groups, so `hex_id` is a visualization
189
+ and grouping key rather than a uniform global hex grid.
190
+
191
+ Land-filtered and compacted exports can contain partial groups when member
192
+ triangles fall outside the coverage or are represented at another level.
193
+ Grouped features include `triangle_count` to make this explicit.
194
+
195
+ ---
196
+
197
+ ## 3. Grid products
198
+
199
+ Built against Natural Earth 1:50m land, base level 6 (~110 km edges):
200
+
201
+ | product | cells | GeoJSON | TopoJSON |
202
+ |---|---:|---:|---:|
203
+ | **uncompacted** — every level-6 cell touching land | 27,614 | 14 MB | 9 MB |
204
+ | **compacted** — interior cells merged up the quadtree as far as they stay wholly on land; coast stays at level 6 | 10,046 | 6 MB | 3.5 MB |
205
+
206
+ Both cover the identical 171.1M km² (149M km² of land + the seaward
207
+ overhang of coastal cells), verified to 0 invalid geometries. Per-cell
208
+ properties: `id` (compact), `path`, `addr64`, `rhombus_id`,
209
+ `rhombus_hilbert`, `hex_id`, `level`, `interior`,
210
+ `edge_km`, `area_km2`, `pole`, `xam`.
211
+
212
+ TopoJSON is the recommended interchange form for grids: every triangle
213
+ edge is shared by two cells, so arc deduplication cuts size ~40–60%. To
214
+ make arcs shared even between *different-sized* neighbours in the
215
+ compacted grid, edges are densified by recursive midpoint subdivision to a
216
+ fixed sub-lattice — a large cell's boundary passes through its small
217
+ neighbours' vertices bit-exactly.
218
+
219
+ ### Special cases
220
+
221
+ * **Antimeridian.** Cells crossing ±180° are written with *continuous*
222
+ longitudes (e.g. `176 → 184`). This intentionally deviates from RFC 7946
223
+ §3.1.9 ("should be split"): splitting would destroy triangle semantics
224
+ and TopoJSON arc sharing, and MapLibre/Leaflet/deck.gl all render
225
+ continuous longitudes correctly. Cells carry `xam: true` so you can
226
+ re-split for strict-RFC consumers if needed. Classification of these
227
+ cells runs against land copies translated ±360°.
228
+ * **Poles.** A pleasing accident of the icosahedron's geometry: in this
229
+ orientation both poles are *lattice vertices* (the south pole is exactly
230
+ the normalized midpoint of an icosahedron edge), so six triangles meet
231
+ at each pole. They are exported as meridian wedges reaching exactly ±90°
232
+ — like UTM-zone tips — and flagged `pole: "vertex"`. Classification near
233
+ the poles runs in polar azimuthal-equidistant frames, where lon/lat
234
+ pathologies do not exist.
235
+ * **No samples, no shortcuts.** Land/sea classification is exact polygon
236
+ containment in an appropriate frame, not point sampling.
237
+
238
+ ---
239
+
240
+ ## 4. Suitable uses and limitations
241
+
242
+ * **Lossless multi-resolution aggregation.** Sum level-9 statistics into
243
+ level-6 cells and the numbers are *exact* — no boundary slivers, no
244
+ overlap weighting. This differs from non-congruent hexagonal hierarchies.
245
+ * **Variable-resolution coverage** (the compacted mode): one dataset,
246
+ coarse where uniform, fine where it matters, with cells that retain
247
+ shared boundaries. Database range scans over `addr64` retrieve
248
+ any subtree as one interval.
249
+ * **Simplicial data structures.** Triangles are *the* primitive of
250
+ numerical geometry: FEM/FVM meshes, terrain TINs, barycentric
251
+ interpolation, subdivision surfaces. A triangular DGGS plugs into that
252
+ machinery directly; quads and hexes need conversion.
253
+ * **Geodesic properties.** Cells are quasi-equilateral
254
+ everywhere — no polar singularity, no latitude-dependent area collapse
255
+ (a lon/lat grid cell at 80°N has ~17% of its equatorial area; Trifold
256
+ cells vary ~±20% worldwide, smoothly).
257
+ * **Sampling designs and ecology-style survey grids**, where equal-ish
258
+ area and hierarchical refinement matter more than neighbour traversal.
259
+
260
+ ### Limitations
261
+
262
+ * **Neighbour-heavy algorithms.** A triangle has 3 edge-neighbours but 9
263
+ more vertex-neighbours, and alternating up/down orientation makes
264
+ "movement" semantics less uniform. Hexagonal grids provide 6 uniform
265
+ neighbours for diffusion, routing, cellular automata, and related
266
+ analyses. (Neighbour traversal across icosahedron face boundaries is
267
+ also unimplemented here — see roadmap.)
268
+ * **Choropleth presentation.** Triangle boundaries can be visually
269
+ prominent. Hexagonal grids may be easier to read for general-audience
270
+ choropleths.
271
+ * **Anisotropy-sensitive statistics.** Up- and down-pointing cells are
272
+ congruent but rotated 60°; kernel-based methods that assume identical
273
+ cell orientation need care.
274
+ * **Local analysis.** At city scale and below, a projected CRS and planar
275
+ grid may be simpler than a global DGGS.
276
+
277
+ ---
278
+
279
+ ## 5. The demo
280
+
281
+ `docs/index.html` (GitHub Pages-ready, https://jaakla.github.io/trifold/)
282
+ — a single self-contained landing page: the full introduction (concept,
283
+ addressing, comparison, use cases, serving) with the interactive viewer
284
+ embedded as its centerpiece:
285
+
286
+ * **7 systems**: Trifold T3 triangles, [A5](https://a5geo.org) pentagons,
287
+ H3 hexagons, S2 quads (s2sphere), rHEALPix (aperture 9,
288
+ near-equal-area), HTM octahedral triangles (a related astronomy grid,
289
+ built with T3's own machinery), and lon/lat rectangles — same land,
290
+ same styling and land mask;
291
+ * **globe ↔ flat** toggle (MapLibre GL v5 native globe and Mercator
292
+ projections);
293
+ * compacted ↔ uncompacted, three triangle resolutions, click-for-address.
294
+
295
+ A presentation can compare the systems in this order: lon/lat in Mercator
296
+ and globe projections, S2, H3, A5, rHEALPix, HTM, and Trifold compacted.
297
+ This sequence shows projection effects, area variation, parent-child
298
+ geometry, and compact addressing.
299
+
300
+ ---
301
+
302
+ ## 6. Comparison with other DGGS
303
+
304
+ | | **Trifold** (this) | **A5** (pentagon) | **H3** (hex) | **S2** (square) | **rHEALPix** | **Geohash / slippy** |
305
+ |---|---|---|---|---|---|---|
306
+ | cell shape | spherical triangle | equilateral pentagon | hexagon (+12 pentagons) | curvilinear quad | quad (squashed at caps) | lon/lat rect |
307
+ | aperture | 4 | 4 (logical) | 7 | 4 | 9 | 4 (slippy) / 32 (geohash) |
308
+ | **exact parent⊃child nesting** | **yes** | no (logical only, index-exact) | **no** (≈7 children, ragged) | yes (within face) | yes | yes (but planar) |
309
+ | equal area | ~±20%, smooth | **exactly equal** per level | ~±35% across res; pentagons differ | up to ~2× corner/centre | **exactly equal-area** | varies with latitude |
310
+ | neighbours | 3 edge + 9 vertex, mixed | 5, two distance classes | **6 uniform** | 4 + 4 | 4 + 4 | 4 + 4 |
311
+ | pole handling | vertex wedges | regular cells | regular cells | face vertices | polar caps | **singular / degenerate** |
312
+ | index arithmetic | uint64, prefix = subtree | uint64, Hilbert | uint64 | uint64, Hilbert | string/int | string prefix |
313
+ | ecosystem | this repository | introduced in 2025 | widely used (Uber, DuckDB, BigQuery…) | widely used (Google, S2geometry) | academic, OGC-adopted | widely used |
314
+ | typical uses | lossless hierarchy, simplicial/FEM work, multi-resolution coverage | equal-area statistics and visualization | neighbour operations, visualization, analytics joins | indexing, range queries, storage | equal-area statistics | tiling and prefix lookup |
315
+
316
+ Selection depends on the application: **H3 provides uniform neighbour
317
+ traversal and a mature ecosystem; S2 focuses on spatial indexing;
318
+ rHEALPix and [A5](https://a5geo.org) provide equal-area cells.** Trifold
319
+ focuses on exact hierarchical aggregation, variable-resolution tilings,
320
+ and pipelines based on triangular geometry. The demo provides a visual
321
+ comparison of these properties.
322
+
323
+ Kin and prior art: OGC DGGS Abstract Specification (Topic 21); ISEA3H /
324
+ DGGRID (icosahedral, aperture 3/4 hex); QTM (Dutton's Quaternary
325
+ Triangular Mesh, an octahedron-based related scheme);
326
+ SCENZ-Grid; HTM (Hierarchical Triangular Mesh, used in astronomy — also
327
+ triangular aperture-4 and octahedron-based; Trifold uses an icosahedron,
328
+ compact addressing, and web tooling. The demo includes
329
+ an octahedral HTM layer for comparison); and
330
+ [**A5**](https://a5geo.org) (Felix Palmer, 2025) — a dodecahedron-based
331
+ pentagonal DGGS with a different hierarchy and area trade-off. It trades
332
+ exact geometric nesting (its aperture-4 hierarchy is logical, with exact
333
+ *index* prefixes but only approximate parent/child geometry) for
334
+ **exactly equal-area cells** within each level via a Snyder-derived
335
+ equal-area projection. Both systems use 64-bit integer indexing. A5 is
336
+ included in the demo's comparison mode (`scripts/build_a5_layer.py`, using
337
+ the official
338
+ [`pya5`](https://pypi.org/project/pya5/) library).
339
+
340
+ ---
341
+
342
+ ## 7. Serving at scale
343
+
344
+ Embedded TopoJSON is used in the demo for datasets up to about 30k cells
345
+ or 10 MB. Larger datasets can use either of these serverless approaches:
346
+
347
+ **Pregenerated PMTiles** — `scripts/make_pmtiles.sh` converts grid products
348
+ to single-file vector-tile archives via tippecanoe and copies them to
349
+ `docs/data/` for GitHub Pages. `make_site.py` detects matching archives in
350
+ `data/` and uses them instead of embedding the corresponding TopoJSON.
351
+ Set `TRIFOLD_PMTILES_BASE_URL` when the archives are hosted separately. Level 8 (~28 km,
352
+ ~440k land cells) tiles to a few tens of MB and supports full-grid display.
353
+
354
+ **Dynamic generation** — `worker/cell-server.js`, deployable free with
355
+ `npx wrangler deploy`. No stored data: cells are regenerated from pure
356
+ math on every request and cached at the edge (`/cell/TF6958`,
357
+ `/locate/lon,lat?level=N`, `/children/…`, `/cells/a,b,c`). This supports
358
+ applications that know which addresses they need, for example from a
359
+ database join on `addr64`, and fetch geometry lazily. The two approaches
360
+ can be combined: PMTiles for full-grid display and the Worker for
361
+ interactive lookup.
362
+
363
+ ---
364
+
365
+ ## 8. Subproject: landcheck — offline land/sea lookup
366
+
367
+ A practical demonstration of exact nesting: the level-10 land
368
+ classification (~6.15M land-touching cells) collapses into 153,884
369
+ run-length intervals over the canonical cell index space — a **182 KB**
370
+ bundled dataset that answers *"is this point on land?"* in ~1–13 µs,
371
+ offline, in Python and JavaScript with identical results and a
372
+ calibrated confidence per answer (measured 99.82% agreement with exact
373
+ polygon containment; all residual error confined to self-flagged
374
+ `coast` answers). An optional second file refines coastal answers with
375
+ OSM simplified land polygons clipped per cell. See
376
+ [landcheck/](landcheck/) and the
377
+ [live in-browser demo](https://jaakla.github.io/trifold/landcheck.html)
378
+ (classify sample or your own points on a map, with measured lookup rate).
379
+
380
+ ---
381
+
382
+ ## 9. Repository layout
383
+
384
+ ```
385
+ src/trifold/ library: address.py · core.py · classify.py · grid.py · cli.py
386
+ scripts/ build_grids.py · build_comparison_dggs.py · build_a5_layer.py · build_more_dggs.py · make_site.py · make_pmtiles.sh
387
+ worker/ cell-server.js (Cloudflare Worker, zero-data cell API)
388
+ landcheck/ offline land/sea point lookup (Python + JS + 182 KB data)
389
+ docs/ index.html (landing page + demo — GitHub Pages ready) ·
390
+ t3-technical-reference.md · img/
391
+ data/ generated products (gitignored; see data/README.md)
392
+ tests/ test_address.py
393
+ ```
394
+
395
+ Quickstart:
396
+
397
+ ```bash
398
+ poetry install --all-extras
399
+ poetry run pytest tests/
400
+ poetry run python scripts/build_grids.py --levels 4 5 6
401
+ poetry run python scripts/build_comparison_dggs.py
402
+ poetry run python scripts/build_a5_layer.py
403
+ poetry run python scripts/build_more_dggs.py
404
+ poetry run python scripts/make_site.py # → docs/index.html
405
+ ```
406
+
407
+ After `eval "$(poetry env activate)"`, omit `poetry run`:
408
+
409
+ ```bash
410
+ pytest tests/
411
+ python scripts/build_grids.py --levels 4 5 6
412
+ python scripts/build_comparison_dggs.py
413
+ python scripts/build_a5_layer.py
414
+ python scripts/build_more_dggs.py # S2 + rHEALPix + HTM layers
415
+ python scripts/make_site.py # → docs/index.html
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Development environment
421
+
422
+ Poetry is the recommended development environment:
423
+
424
+ ```bash
425
+ poetry install --all-extras
426
+ eval "$(poetry env activate)" # activate in the current zsh/bash session
427
+ ```
428
+
429
+ Poetry 2.x no longer includes `poetry shell` by default. It manages this
430
+ environment itself and may not install `pip`; do not run pip installation
431
+ commands while the prompt starts with `(trifold)`. Use `poetry add` to add
432
+ dependencies, or `poetry run COMMAND` without activating the environment.
433
+
434
+ Alternatively, use a standard virtual environment instead of Poetry:
435
+
436
+ ```bash
437
+ deactivate 2>/dev/null || true # leave the Poetry environment first
438
+ python -m venv .venv-pip
439
+ source .venv-pip/bin/activate
440
+ python -m ensurepip --upgrade
441
+ python -m pip install -e ".[build,dev]"
442
+ ```
443
+
444
+ `tippecanoe` is required for `scripts/make_pmtiles.sh` (OS-level tool):
445
+
446
+ ```bash
447
+ # macOS (Homebrew)
448
+ brew install tippecanoe
449
+
450
+ # Linux: build from source or use the docker image; the script assumes `tippecanoe` is on PATH
451
+ ```
452
+
453
+ PMTiles are built from generated GeoJSON files. Generate those files
454
+ first, then run `tippecanoe` through the wrapper:
455
+
456
+ ```bash
457
+ python scripts/build_grids.py --levels 6
458
+ ./scripts/make_pmtiles.sh # all discovered global_tri_L*; skips existing archives
459
+ python scripts/make_site.py
460
+ ```
461
+
462
+ The wrapper auto-discovers every `data/global_tri_L*_*.geojson` product —
463
+ nothing is hardcoded. By default it skips grids whose `.pmtiles` already
464
+ exists; pass `--force` to rebuild. Restrict to specific levels with
465
+ `--levels` (accepts `N`, `N-M`, `N-`, or `-M`):
466
+
467
+ ```bash
468
+ ./scripts/make_pmtiles.sh --levels 7 # just L7
469
+ ./scripts/make_pmtiles.sh -l 4-8 # L4 through L8
470
+ ./scripts/make_pmtiles.sh -l 6- --force # L6 and up, rebuild even if present
471
+ ./scripts/make_pmtiles.sh --help # full usage
472
+ ```
473
+
474
+ The wrapper writes archives under both `data/` and `docs/data/`. The site
475
+ generator detects matching PMTiles in `data/` and embeds
476
+ TopoJSON only for datasets without an archive.
477
+
478
+ The grid build requires the Natural Earth checkout described in
479
+ Natural Earth 1:50m land data. The default build downloads and verifies the
480
+ pinned v5.1.2 GeoJSON automatically. Use `--land PATH` to supply another
481
+ local dataset instead.
482
+
483
+ ## 10. Roadmap
484
+
485
+ * neighbour traversal across face boundaries (edge-adjacency tables)
486
+ * level 7–9 products + PMTiles in CI
487
+ * vectorized `locate` DuckDB UDF for `addr64` joins
488
+ * optional ISEA-style equal-area variant (snyder projection per face)
489
+ * polygon→cells fill (`polyfill` equivalent)
490
+
491
+ ## License
492
+
493
+ MIT. Land data: [Natural Earth](https://www.naturalearthdata.com/) (public
494
+ domain). Built with shapely, pyproj, geopandas, topojson, MapLibre GL,
495
+ topojson-client, H3 (comparison layer).
@@ -0,0 +1,14 @@
1
+ t3grid-0.1.0.dist-info/licenses/LICENSE,sha256=2cFqNsp4BO_7roCcuGQKmZMWv5VcU0AyH-rdRhkyGWM,1077
2
+ trifold/__init__.py,sha256=ZHYr0GkY5m4FFDJwyjInsIcSrRTJ8qtadGJNRVABDzU,326
3
+ trifold/address.py,sha256=UPH2XbKGx0Kbg0LfHknaAXayZsBypQhnFauiNl_YTUc,12670
4
+ trifold/api.py,sha256=5GKq4kVn3eeaxR0i0dRMnuCILt2h6_dBA_bZ2etK2r0,7539
5
+ trifold/classify.py,sha256=HeBKLYmk7EInLqLJ48kyIQplL15tP6DGjDO8zv2kSFk,5141
6
+ trifold/cli.py,sha256=sbibJHZNk52Lsfj_xuojbDk5SNXJgtwh3UKCzegWx7o,1964
7
+ trifold/core.py,sha256=ZQJOxU3O2vASePnsxKoaIaKE-vNd8GCsxKBSiMW_Zts,16807
8
+ trifold/grid.py,sha256=5jyCY6Y2AHNBo0FHwO-VqLT5ZmSUuHqgtt6ncngeWTU,2738
9
+ trifold/land.py,sha256=pz4QnBPsBXWBn0A9lG-kDdavcgNsuhXGuAiCd_6HD8Q,306
10
+ t3grid-0.1.0.dist-info/METADATA,sha256=8cm0hCIRNBWYq11LK_5mQp_77cfr3DPH65_vXouQUpE,23057
11
+ t3grid-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ t3grid-0.1.0.dist-info/entry_points.txt,sha256=lGseot9U9Ht-pfvB_QSsg-iyxMo_7N7fiSFqdqR_r6I,45
13
+ t3grid-0.1.0.dist-info/top_level.txt,sha256=5VfJckOqMEpiJ1umV74eIoUA5sDxRqug2oIym5LIaSU,8
14
+ t3grid-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trifold = trifold.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TriGrid contributors
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 @@
1
+ trifold
trifold/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Trifold Python SDK.
2
+
3
+ The supported public API is defined in :mod:`trifold.api` and re-exported
4
+ here for concise imports. Land classification is available separately as
5
+ ``trifold.land.LandClassifier``.
6
+ """
7
+ __version__ = "0.1.0"
8
+
9
+ from .api import *
10
+ from .api import __all__ as _api_all
11
+
12
+ __all__ = ["__version__", *_api_all]