Grid Raster Tessellation
GeoBrix provides a unified model for mapping raster tiles onto discrete global grids — H3 hexagons, quadbin tiles, BNG squares, and custom grids. Every raster-to-grid function (rst_<grid>_tessellate, rst_<grid>_rastertogrid*) shares three orthogonal, explicit policies: coverage, assignment, and fill. This page explains the model, per-grid specifics, custom-grid reproducibility rules, the interpolation decision guide, and the cross-tier guarantee.
The common model
Coverage: {sparse, complete} (default: complete)
Coverage controls whether grid cells that are geometrically covered by the raster footprint — but contain no valid pixel data — appear in the output.
complete(default): every cell whose geometry overlaps the raster bounding box is emitted. A covered cell with no valid pixels is emitted with aNULLmeasure and a pixel count of0. Downstream, anyNULLin the result is provably genuine missing data, not a centroid-aliasing artifact.sparse: covered-but-empty cells are dropped. The output contains only cells that caught at least one valid pixel.
Three states are always distinguishable under coverage='complete':
| State | Output |
|---|---|
| Has valid pixels | (cellid, value) — count > 0 |
| Covered, all-NoData | (cellid, NULL) — count = 0, row present |
| Outside raster footprint | Absent — no row emitted |
The footprint used for covered-cell enumeration is the raster bounding box, not a NoData-aware region. A cloud hole entirely inside the raster correctly reads as covered + NULL, not out-of-coverage.
Assignment: {centroid, covering} (default: centroid)
Assignment controls how pixels are distributed to cells.
centroid(default): each valid pixel is mapped to the single cell containing that pixel's centroid coordinate. Pixels partition into cells — every valid pixel appears in exactly one output chip, no pixel is counted twice, and the union of all chips reproduces the full valid-pixel set.covering— the meaning differs by function family:- Chip-emitting (
rst_<grid>_tessellate): covering-set clip. A cell is emitted if and only if its geometry has positive-area overlap with the raster; a boundary-touch with zero pixel overlap is excluded. A within-extent cell that contains only NoData pixels is still emitted (NoData renders in place). - Scalar-reducing (
rst_<grid>_rastertogrid*): area-weighted. Each pixel contributes to every cell its footprint overlaps, weighted by the fraction of the pixel's area inside that cell. Interior pixels (entirely within one cell) carry weight 1; only boundary pixels split. This fills centroid-aliasing gaps — a cell that catches no centroid incentroidmode receives area-weighted contributions undercovering.sumandcountremain globally mass-conserving.
- Chip-emitting (
The interaction between the two policies:
centroid + complete: covered cells with no assigned centroid appear asNULL— aliasing gaps are visible but not closed.covering + complete: aliasing gaps are closed without interpolation — the onlyNULLcells are those where every overlapping pixel was NoData. (Mechanism differs by family: covering-set clip for tessellate, area-weighted for rastertogrid*; the coverage policy and null semantics are identical.)
When to use centroid (default): binning a collection of overlapping tiles without double-counting; building a deduplication pipeline; aggregating a time-series composite where each pixel should be counted once.
When to use covering — tessellate family: building a full-coverage index across tile boundaries; reconstructing a complete raster from chips; matching the semantics of Databricks-native h3_tessellateaswkb (which is also a covering-set clip). For the rastertogrid* family: area-weighted assignment — each pixel contributes fractionally to every cell it overlaps — is useful when you need mass-conserving totals rather than single-cell centroid assignment.
Fill: always a separate explicit step
Fill is never automatic. No rst_<grid>_tessellate or rst_<grid>_rastertogrid* call fills gaps unless you invoke a fill function explicitly. This is a deliberate guarantee: a query that omits any fill step provably did not interpolate.
Two fill granularities are available:
- Pixel space (pre-tessellation):
rst_fillnodatafills NoData pixels from neighboring pixels using GDAL inverse-distance weighting. Apply it to the raster before calling anyrst_<grid>_tessellateorrst_<grid>_rastertogrid*function. - Cell space (post-aggregation):
gbx_<grid>_cellfill(cellid, value, [k], [method], [power])fills covered-but-NULLcells from valid cells withinkrings of the grid. Apply it to the(cellid, value)table produced by anyrastertogrid*function.
See the Interpolation and fill section for the full boundary guide.
Determinism: one boundary rule across all grids
One tie-break rule applies uniformly across H3, quadbin, BNG, and custom grids: lower-cell-owns, upper-exclusive. A pixel centroid that falls exactly on a shared cell boundary goes to the cell with the lower coordinate — never split, never double-counted. This rule is enforced by the shared grid abstraction so results are reproducible regardless of grid type.
Relationship to rst_<grid>_rastertogrid*
The raster-to-grid surface has two output shapes:
rst_<grid>_tessellate(...) | rst_<grid>_rastertogrid* | |
|---|---|---|
| Output per cell | Raster chip (clipped tile struct) | Scalar measure (avg / count / max / min / median) |
| Return type | One row per cell via UDTF | ARRAY<ARRAY<STRUCT(cellid, measure)>> per band |
| coverage param | shared | shared |
| assignment param | shared | shared |
| Use for | Per-cell raster chips for downstream raster ops | Numeric aggregation into the grid |
Both function families share the same pixel-assignment logic. rst_<grid>_tessellate(..., assignment='centroid') assigns pixels by the same centroid rule as rst_<grid>_rastertogridavg — the difference is the output: chips versus scalar measures.
-- chip-emitting (default assignment=centroid, coverage=complete)
SELECT t.*
FROM rasters,
LATERAL gbx_rst_h3_tessellate(tile, 7) t;
-- scalar-reducing (same pixel assignment, same coverage default)
SELECT path, gbx_rst_h3_rastertogridavg(tile, 7) AS h3_avg
FROM rasters;
Per-grid details
- H3
- Quadbin
- BNG
- Custom
Cell-id type: BIGINT — a standard H3 integer cell ID.
CRS handling:
rst_h3_tessellatereprojects the tile extent to EPSG:4326 internally before H3 cell lookups. Any CRS supported by GDAL is accepted; no pre-transformation of the tile is required.rst_h3_rastertogrid*interprets pixel coordinates as EPSG:4326 lon/lat directly. If your tiles are in a projected CRS (e.g., UTM), reproject them upstream withrst_transformbefore calling anyrst_h3_rastertogrid*function.
Resolution: an integer level 0–15 (higher = finer hexagons; level 7 ≈ 5 km² per cell; level 9 ≈ 0.1 km²).
Available functions:
gbx_rst_h3_tessellate(tile, resolution, [assignment], [coverage])— UDTF, one chip per cellgbx_rst_h3_rastertogrid{avg,count,max,min,median,sum,stddev,variance}(tile, resolution, [coverage], [assignment])— scalar aggregationgbx_rst_h3_rasterize_agg— grouped (GROUP BY) aggregator: rasterizes each cell'svalue(or a1.0presence mask when null) at the cell location into one tile per group (cells→raster inverse direction). See the reference for the full signature.gbx_h3_cellfill(cellid, value, [k], [method], [power])— post-aggregation cell-space fill
Lineage: the raster-to-H3 technique was pioneered by DBLabs Mosaic (rst_tessellate, rst_rastertogridavg/count/max/min/median). Databricks has since adopted the same grid for vector operations — see the H3 geospatial functions reference (AWS · Azure · GCP):
h3_coverash3— returns every H3 cell that overlaps a geometry (the covering set)h3_tessellateaswkb— for each covering-set cell, returns the geometry clipped to that cell
GeoBrix carries the raster side of this family and is designed to complement, not replace, the native h3_* functions. Use GeoBrix rst_h3_tessellate / rst_h3_rastertogrid* for raster data onto an H3 grid; use Databricks-native h3_coverash3 / h3_tessellateaswkb for vector geometry operations on the same grid.
The cellid in each tessellated output row is a standard H3 integer — join or aggregate it directly against Databricks-native H3 functions in the same query:
-- join raster chips with vector H3 data
SELECT chips.cellid, chips.tile, vectors.name
FROM (
SELECT t.*
FROM rasters,
LATERAL gbx_rst_h3_tessellate(tile, 7) t
) chips
JOIN h3_indexed_vectors vectors
ON chips.cellid = vectors.h3_cell_id;
-- mean elevation per H3 cell from chips
SELECT cellid, AVG(gbx_rst_avg(tile)) AS mean_elevation
FROM (
SELECT t.*
FROM dem_tiles,
LATERAL gbx_rst_h3_tessellate(tile, 7) t
)
GROUP BY cellid;
See the Helios notebooks for a worked example of gbx_rst_h3_rastertogridavg binning slope and aspect rasters into H3 cells to produce a per-cell score (NB03).
Cell-id type: BIGINT — a Quadbin integer cell ID.
CRS handling: tiles are reprojected to geographic coordinates internally. Any CRS supported by GDAL is accepted; no pre-transformation required.
Resolution: an integer zoom level 0–26 (higher = finer tiles). Quadbin cells align with Web Mercator tile boundaries.
Available functions:
gbx_rst_quadbin_tessellate(tile, resolution, [assignment], [coverage])— UDTF, one chip per cellgbx_rst_quadbin_rastertogrid{avg,count,max,min,median,sum,stddev,variance}(tile, resolution, [coverage], [assignment])— scalar aggregationgbx_rst_quadbin_rasterize_agg— grouped (GROUP BY) aggregator: rasterizes each cell'svalue(or a1.0presence mask when null) at the cell location into one tile per group (cells→raster inverse direction). See the reference for the full signature.gbx_quadbin_cellfill(cellid, value, [k], [method], [power])— post-aggregation cell-space fill
Cell-id type: STRING — a human-readable BNG cell reference (e.g., TQ38, TQ3882).
CRS handling: tiles are reprojected to EPSG:27700 (British National Grid) internally. Any CRS supported by GDAL is accepted.
Resolution: an integer index 1–6 (1 = 100 km, 2 = 10 km, 3 = 1 km, 4 = 100 m, 5 = 10 m, 6 = 1 m), or a string key from the BNG resolution map (e.g., "1km", "100m"). Negative indices (−1 to −6) select quadrant sub-cells.
Available functions:
gbx_rst_bng_tessellate(tile, resolution, [assignment], [coverage])— UDTF, one chip per cellgbx_rst_bng_rastertogrid{avg,count,max,min,median,sum,stddev,variance}(tile, resolution, [coverage], [assignment])— scalar aggregationgbx_rst_bng_rasterize_agg— grouped (GROUP BY) aggregator: rasterizes each cell'svalue(or a1.0presence mask when null) at the cell location into one tile per group (cells→raster inverse direction). See the reference for the full signature.gbx_bng_cellfill(cellid, value, [k], [method], [power])— post-aggregation cell-space fill
Cell-id type: BIGINT — an origin-relative integer ID. Custom cell IDs are not self-describing and not comparable across different grid definitions; see Custom-grid determinism below.
CRS handling: the custom grid defines its own coordinate system. Tiles are expected to share the grid's CRS; no automatic reprojection is applied.
Resolution: the grid struct defines the grid's origin, bounds, and root cell size; the separate resolution argument selects the subdivision level, filling the same role as for H3, quadbin, and BNG grids.
Available functions:
gbx_rst_custom_tessellate(tile, grid, resolution, [assignment], [coverage])— UDTF, one chip per cellgbx_rst_custom_rastertogrid{avg,count,max,min,median,sum,stddev,variance}(tile, grid, resolution, [coverage], [assignment])— scalar aggregationgbx_rst_custom_rasterize_agg— grouped (GROUP BY) aggregator: rasterizes each cell'svalue(or a1.0presence mask when null) at the cell location into one tile per group (cells→raster inverse direction). See the reference for the full signature.gbx_custom_cellfill(cellid, value, grid, [k], [method], [power])— post-aggregation cell-space fill
Custom-grid determinism
Custom grids assign integer cell IDs using origin-relative arithmetic — (x − boundXMin) / cellWidth. These IDs are not self-describing and not comparable across different grid definitions: a cell ID produced by one grid struct cannot be compared to a cell ID produced by a different grid struct, even if both structs use the same cell size. Four rules govern reproducible use:
R1 — carry the grid struct with any output. Cell IDs are only meaningful relative to the grid struct that produced them. Any downstream join, aggregation, or storage operation that includes custom-grid cell IDs must carry the grid struct so that joins remain valid and IDs can be interpreted. Storing cell IDs without the grid struct makes the data uninterpretable in isolation.
R2 — fix bounds from a stable reference, not from the data. Define the grid bounding box from a known geographic reference (e.g., a named study-area extent or a projection's natural bounds), never from the per-dataset raster extent. A dataset-derived extent shifts when input data changes, producing cell IDs that are silently incomparable across runs.
R3 — bounds must strictly contain the raster extent. If the raster extent falls outside the declared grid bounds, the function raises a clear error immediately — before any pixel is processed — rather than failing mid-aggregation when a boundary pixel triggers an out-of-bounds lookup. Validate your bounds before running large jobs.
R4 — lower-cell-owns, upper-exclusive (universal). This is the boundary tie-break rule for all grids (H3, quadbin, BNG, and custom). For custom grids specifically, a pixel centroid coordinate exactly equal to the declared upper bound falls outside the grid and triggers an error rather than being silently assigned.
Interpolation and fill
Several functions operate in the interpolation and gap-fill space. The table below draws explicit boundaries so you can choose the right tool at each stage of the pipeline:
| Function | Domain | What it touches | Reach for it to… |
|---|---|---|---|
rst_sample | raster → point | reads values (nearest-pixel) | extract pixel values at point locations |
rst_resample* | raster → raster | all pixels | change pixel resolution / grid density |
rst_fillnodata | raster (pixel) | only NoData pixels, pre-tessellation | fill pixel holes from neighbour pixels |
gbx_<grid>_cellfill | grid (cell) | only NULL cells, post-aggregation | fill missing grid cells from k-ring neighbour cells |
rst_gridfrompoints* | points → raster | builds a new raster (IDW) | interpolate a raster from scattered point samples |
rst_filter / rst_convolve | raster → raster | all pixels | focal / neighbourhood stats & kernels (not gap-fill) |
Rule of thumb: sample reads, resample changes resolution, fill replaces only the missing and leaves valid data untouched (pixel stage rst_fillnodata, cell stage cellfill), gridfrompoints builds from points, focal transforms every pixel.
Geometry-aware expansion (geomkring / geomkloop)
The model above maps a raster onto grid cells. The geometry-aware ring functions do the complementary vector operation: they expand grid cells outward from a vector geometry, so you can grow a buffer band, an inward setback, or a hole fill entirely in cell space. k=0 is the geometry's covering set — the same polyfill this page describes — and each step dilates that set by one ring of grid-topology neighbours (a Minkowski sum of the covering set with a k-disk, not N independent single-cell rings).
gbx_<grid>_geomkring(geom, resolution, k [, mode])— the filled disk: the covering set plus every cell withinksteps.gbx_<grid>_geomkloop(geom, resolution, k [, mode])— the hollow shell: only the cells at exactlyksteps.*explodevariants stream one row per cell via SQLLATERAL.
A single trailing-optional mode enum selects how the walk treats the geometry's outer boundary and its holes (default 'boundary-out', backward-compatible):
mode | Seed → walk | Use |
|---|---|---|
'boundary-out' (default) | outer boundary → outward | outward buffer band on/outside the edge |
'boundary-in' | outer boundary → inward, respecting holes | inner setback |
'boundary-in-ignore-holes' | outer boundary → inward, polygon treated as solid | inner setback, holes ignored |
'hole-in' | hole boundary → inward | fill each hole from its edge |
'hole-out' | hole boundary → outward into the solid | band around each hole |
'hole-out-ignore-geom' | hole boundary → outward, unbounded by the outer ring | outward buffer from each hole |
All four grids ride one shared dilation engine, so the modes and semantics are identical across H3, quadbin, BNG, and custom. BNG, quadbin, and custom run on both tiers; H3 is light-tier only (geometry-taking, via the h3 library). Cell IDs are BIGINT (H3/quadbin/custom) or STRING (BNG); the custom variants take the grid struct as their second argument. See GridX Functions for the per-grid signatures and Benchmarking § Geometry-aware expansion at scale for throughput/scaling.
Cross-tier parity
For coverage/assignment cell-set semantics and computed values, behavior is identical across the heavyweight (rasterx / Scala JAR) and lightweight (pyrx / pure Python) tiers for all grids:
- The same
{coverage, assignment}parameters produce the same cell sets and the same per-cell values on both tiers. - Covered-but-missing cells are emitted with
NULLmeasures undercoverage='complete'on both tiers. - For
rastertogrid*, area-weightedcoveringproduces mass-conserving weights on both tiers. Fortessellate,coveringis a covering-set clip — cells are emitted iff they have positive-area overlap with the raster. Both behaviors are verified by cross-tier parity tests.
Grouped-aggregator return-type difference: the grouped aggregators (<grid>_cellfill and rst_<grid>_rasterize_agg) return the full struct or tile on the heavyweight tier, but return BINARY on the lightweight SQL tier — a grouped-aggregate pandas_udf cannot return a StructType. The lightweight Python wrappers (not SQL) return the full struct.
Switch tiers with a one-line import change; the function calls are identical:
- Lightweight (pyrx)
- Heavyweight (rasterx)
from databricks.labs.gbx.pyrx import functions as rx # lightweight tier
chips = df.select(rx.rst_h3_tessellate("tile", 7))
chips = df.select(rx.rst_h3_tessellate("tile", 7, assignment="covering", coverage="complete"))
from databricks.labs.gbx.rasterx import functions as rx # heavyweight tier
chips = df.select(rx.rst_h3_tessellate("tile", 7))
chips = df.select(rx.rst_h3_tessellate("tile", 7, assignment="covering", coverage="complete"))
Parity is test-enforced per function: for each grid and each assignment mode, tests assert that the lightweight and heavyweight tiers produce the same cell set and the same per-cell chip pixels on border-containing tiles.