Skip to main content

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.5.2-b5, 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 — y increases southward from the top-left origin. OpenStreetMap / MapLibre / Google. Not TMS, whose y increases 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), FeatureEntity, FeatureLoader.
  • collection — TileCollection, TileEntity, LayerDefinition, ViewportRequest.
  • scope — check_cache_scope, MAX_CACHE_SCOPE_LENGTH.
  • overlay — KeyedFeature, Overlay, cut, replace_features, missing_features, area_km2, mean_width_m, LayerOverlayError.

threetears.geo.__all__ is the full list.

Every tile source names a cache scope

TileCollection and FeatureCache both require cache_scope, a keyword argument with no default. Their caches are keyed by layer name and version, and nothing in that key says whose layer it is: two sources serving a layer of the same name (two datasources, or a customer's datasource and a platform layer) would otherwise share the pod-local L1 table, the NATS L2 keys, the cross-pod build lock and the registry entry, and whichever built a tile first would answer for both.

The scope goes into the collections' table names (geo_tiles_<scope>, geo_features_<scope>), which are interpolated into SQL and NATS KV keys, so check_cache_scope holds it to a grammar both accept unquoted: a lowercase letter, then lowercase letters, digits and underscores, at most MAX_CACHE_SCOPE_LENGTH characters. ds_<32 hex> for a datasource and ns_<32 hex> for a provider namespace both fit.

Building layers from other layers

threetears.geo.overlay cuts one layer by another, replaces features, and checks coverage. A layer is a sequence of KeyedFeature: a stable id, a WGS84 geometry, and a key that partitions the layer (a state, a country) so only features sharing it are paired. Two layers from different sources disagree at their boundaries by a few meters, so a cut leaves thin slivers and small fragments; each part of a cut is judged on its own and dropped when it is narrower or smaller than the caller's thresholds (mean_width_m, area_km2 are the measures). The thresholds belong to the data: measure them on the layers in hand and pass them in. Measurements use a local equirectangular projection around each shape's own latitude.

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.

Once a rectangle's chunks are covered, the R-Tree answers which held features overlap it, and an exact rectangle test follows (the R-Tree stores 32-bit floats rounded outward, so it can over-answer but never under-answer). What a pod holds is bounded by max_cached_rows and evicted least recently read first, rows and index entries together. One read newly holds at most half the bound, summed over every chunk it loads; the chunks past that, or a wide low-zoom rectangle whose entries would pass it, are answered and not held. A rectangle over more than max_chunk_reads uncovered chunks is one loader call, and with no L1 bound every read is one loader call.

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.

Metadata

Release files for 3tears-geo 0.69.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for 3tears-geo 0.69.0
File Size Uploaded
3tears_geo-0.69.0.tar.gz 62.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for 3tears-geo 0.69.0
File Interpreter ABI Platform
3tears_geo-0.69.0-py3-none-any.whl Python 3 none any Details

Total release size: 103.3 kB

Release files / 3tears_geo-0.69.0.tar.gz

Download URL 3tears_geo-0.69.0.tar.gz
Size 62.4 kB
Tags Source
SHA-256 checksum
How to use checksums
f9b875eb1341b33b00660f9d03fa4b3372527a016b7ab89a1d5cfaa7367356e6
BLAKE2b-256 checksum
How to use checksums
bd4bb66ddd46604443a24fdaddd37fa8a80c19993846ba2a1c399dc22145b2d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / 3tears_geo-0.69.0-py3-none-any.whl

Download URL 3tears_geo-0.69.0-py3-none-any.whl
Size 40.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3643fe7ed5bdaf7c9bc1df40460ed3d1e57d30a25ec69053d4d81e0e56cffeae
BLAKE2b-256 checksum
How to use checksums
6da61e4282c8a0fb93cecd71b475de21a140989a89ddc1d462c11ea3a9512064
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.69.0 This release

2 release files

0.68.0

2 release files

0.67.0

2 release files

0.66.0

2 release files

0.57.0

2 release files

0.56.3

2 release files

0.56.2

2 release files

0.56.1

2 release files

0.56.0

2 release files

0.55.0

2 release files

0.54.0

2 release files

0.53.0

2 release files

0.52.1

2 release files

0.52.0

2 release files

0.51.1

2 release files

0.51.0

2 release files

0.50.0

2 release files

0.49.0

2 release files

0.48.0

2 release files

0.47.1

2 release files

0.47.0

2 release files

0.46.1

2 release files

0.46.0

2 release files

0.45.1

2 release files

0.45.0

2 release files

0.44.0

2 release files

0.43.0

2 release files

0.42.0

2 release files

0.41.4

2 release files

0.41.3

2 release files

0.41.2

2 release files

0.41.1

2 release files

0.41.0

2 release files

0.40.0

2 release files

0.39.0

2 release files

0.38.0

2 release files

0.37.0

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.0

2 release files

0.27.0

2 release files

0.26.1

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.24.7

2 release files

0.24.6

2 release files

0.24.5

2 release files

0.24.4

2 release files

0.24.3

2 release files

0.24.2

2 release files

0.24.1

2 release files

0.24.0

2 release files

0.23.9

2 release files

0.22.4

2 release files

0.22.3

2 release files

0.22.2

2 release files

0.22.1

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.19.4

2 release files

0.19.3

2 release files

0.19.2

2 release files

0.19.1

2 release files

0.19.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page