Slippy-map tile geometry: WKB decoding, zoom bands, and MVT encoding in application code (no PostGIS)
Project description
3tears-geo
Slippy-map tile geometry for the 3tears platform: WKB decoding, zoom bands, and MVT encoding — all in application code.
Why this exists in Python rather than in SQL
Every off-the-shelf tile server (Martin, pg_tileserv, Tegola) assumes
PostGIS in the database and calls ST_AsMVT. YugabyteDB ships no postgis
extension — verified empirically against yugabytedb/yugabyte:2025.2.1.0-b141,
where CREATE EXTENSION postgis fails outright with no control file on the
image. There is therefore no ST_Intersects, no ST_Simplify, and no
ST_AsMVT to call.
So this package does that work: shapely for the geometry, and
mapbox-vector-tile for the encoding. The relevant prior art is Tippecanoe
and supercluster, not the Postgres tile servers.
The tiling scheme, stated explicitly
Leaving this implicit guarantees a defect, because the two common conventions differ only in the direction of one axis:
- Web Mercator (EPSG:3857)
- XYZ orientation —
yincreases southward from the top-left origin. OpenStreetMap / MapLibre / Google. Not TMS, whoseyincreases northward. - Source coordinates are WGS84 (EPSG:4326), projected at build time.
- MVT geometry uses tile-local integer coordinates over a 4096-unit extent, the format default, not varied per layer.
Latitude clamps to ±85.0511287798066°, the bound of the Mercator square.
Public surface
Imported via from threetears.geo import …:
- tiles —
TileId,BoundingBox,tile_for_point,tile_bounds,bounds_to_tile_range,TILE_EXTENT,MAX_MERCATOR_LATITUDE. - attributes —
coerce_attribute,coerce_attributes,validate_attribute_value,UnsupportedAttributeError. - geometry —
decode_geometry,geometry_bounds,point_geometry. - bands —
feature_band,aggregate_band,FeatureSpec,AggregateSpec,TileFeature,simplification_tolerance. - mvt —
encode_tile,project_to_tile. - features —
FeatureCache(per-pod source cache + R-Tree). - collection —
TileCollection,LayerDefinition,ViewportRequest.
Zoom bands: aggregate below, features above
Low zoom is not simplified high zoom. A z4 tile spans a large fraction of a country; rendering it by simplifying and dropping individual features leaves an arbitrary sample of whichever survived. A national view showing 4,000 of 180,000 precincts is not a coarse view of the data — it is a different and misleading dataset.
So each layer declares a crossover. Below it, rows roll up to a coarser
declared geography and each bucket becomes one feature carrying real totals.
Above it, individual features are simplified per zoom and capped in count.
bl-ds-ai-lcv-registration reached the same split independently: its
precomputed z4–z10 band holds cluster aggregates, with individual features
only from z11.
The cap is a hard limit, not advice. An uncapped tile in a dense metro reaches tens of megabytes, which exceeds the NATS payload ceiling, defeats the L2 hot band, and renders badly. When it binds, features drop by a declared ranking column so the survivors are the important ones, and the result records that it was truncated — a silently capped tile reads as "this is all the data there is".
Feature ids are not MVT ids
MVT feature ids are uint64 by specification. A census geoid or a UUID is
silently coerced to 0 by the encoder — no error — which would collapse
every feature in a tile onto one id and break any client-side join. So only
genuine integers reach the wire-level id; everything else travels as a
property, which is exactly what MapLibre's promoteId is for. The id is
always present as a property either way, so a client has one place to look.
The R-Tree earns its place via chunk coverage
FeatureCache indexes cached source features in a SQLite R-Tree (built in,
unlike SpatiaLite) on the same connection pool as its own managed table, as
a BaseCollection subclass rather than a bespoke SQLiteBackend wrapper.
An index alone cannot answer "which features are in this rectangle": it can say what a pod holds, never whether it holds all of them, and a tile built from a partial set is wrong rather than slow — then cached as immutable. So the cache tracks coverage by chunk: it loads the coarse tile containing a request and records that chunk as covered. A run of neighbouring tiles, which overlap almost entirely in source features, then pays one L3 read between them instead of one each.
Attribute coercion is fixed, not per-caller
MVT carries only strings, numbers and booleans, so every other SQL type needs a declared mapping. Two cases are quietly lossy if chosen badly:
| SQL | MVT | Note |
|---|---|---|
| integer / numeric / double | number | doubles are IEEE754 |
| text / varchar | string | |
| boolean | bool | checked before int, since bool subclasses it |
| NULL | key omitted | MVT has no null |
| timestamp / date | string | ISO-8601 UTC; naive stamps read as UTC |
| JSONB / array / bytes | rejected | project a scalar column instead |
Omitting the key for NULL is the only faithful encoding, and it puts a real obligation on the client: a style expression reading that attribute must supply its own fallback. The upside is that "no data" and "a genuine zero" stay distinguishable in the tile — which matters most on a choropleth, where collapsing them shades unmeasured regions as though they were measured.
Versioning policy
3tears-geo versions in lockstep with the rest of the 3tears monorepo:
every package shares one version, tracking the framework git tag. All
packages move together.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file 3tears_geo-0.19.0.tar.gz.
File metadata
- Download URL: 3tears_geo-0.19.0.tar.gz
- Upload date:
- Size: 42.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af9ff153eb8817b1e111add7c0251f8913909192ab00ccc6fa8f584fe759e90b
|
|
| MD5 |
79163459a3da14194d671eddee9ed3a1
|
|
| BLAKE2b-256 |
4134c045e2328b388ef38011e808ac160961ad1213267ec9227fe7e13aae77a6
|
Provenance
The following attestation bundles were made for 3tears_geo-0.19.0.tar.gz:
Publisher:
release.yml on pacepace/3tears
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
3tears_geo-0.19.0.tar.gz -
Subject digest:
af9ff153eb8817b1e111add7c0251f8913909192ab00ccc6fa8f584fe759e90b - Sigstore transparency entry: 2252476176
- Sigstore integration time:
-
Permalink:
pacepace/3tears@e04b8e99b585ebb10ce5de899a22d85e56280f02 -
Branch / Tag:
refs/tags/v0.19.0 - Owner: https://github.com/pacepace
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e04b8e99b585ebb10ce5de899a22d85e56280f02 -
Trigger Event:
push
-
Statement type:
File details
Details for the file 3tears_geo-0.19.0-py3-none-any.whl.
File metadata
- Download URL: 3tears_geo-0.19.0-py3-none-any.whl
- Upload date:
- Size: 29.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6359a22f3f269719a476bbceabde5665c3cf7dde10ec190539adb1316a6c4fba
|
|
| MD5 |
21b8fe22536198541b9f397fa6647a69
|
|
| BLAKE2b-256 |
9db4116e39a8d75a457836a43bea2a2f4cde2969b82aa566e7635e7d85cdb590
|
Provenance
The following attestation bundles were made for 3tears_geo-0.19.0-py3-none-any.whl:
Publisher:
release.yml on pacepace/3tears
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
3tears_geo-0.19.0-py3-none-any.whl -
Subject digest:
6359a22f3f269719a476bbceabde5665c3cf7dde10ec190539adb1316a6c4fba - Sigstore transparency entry: 2252484159
- Sigstore integration time:
-
Permalink:
pacepace/3tears@e04b8e99b585ebb10ce5de899a22d85e56280f02 -
Branch / Tag:
refs/tags/v0.19.0 - Owner: https://github.com/pacepace
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e04b8e99b585ebb10ce5de899a22d85e56280f02 -
Trigger Event:
push
-
Statement type: