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.
- t3grid-0.1.0.dist-info/METADATA +495 -0
- t3grid-0.1.0.dist-info/RECORD +14 -0
- t3grid-0.1.0.dist-info/WHEEL +5 -0
- t3grid-0.1.0.dist-info/entry_points.txt +2 -0
- t3grid-0.1.0.dist-info/licenses/LICENSE +21 -0
- t3grid-0.1.0.dist-info/top_level.txt +1 -0
- trifold/__init__.py +12 -0
- trifold/address.py +360 -0
- trifold/api.py +251 -0
- trifold/classify.py +140 -0
- trifold/cli.py +71 -0
- trifold/core.py +465 -0
- trifold/grid.py +72 -0
- trifold/land.py +10 -0
|
@@ -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
|
+
[](https://github.com/jaakla/trifold/blob/main/docs/t3-technical-reference.md)
|
|
49
|
+
|
|
50
|
+

|
|
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,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]
|