How spatial geometry is stored across the COEQWAL database: the column convention every geometry table shares, which tables keep geometry in a dedicated table versus on the entity row, how it is consumed, and the target layout. The coverage and gap sections focus on demand units (DUs), where the gaps are. Verified against the May 2026 monthly audit snapshot and the current schema.
Every geometry-bearing table shares the same column triad: geom (the PostGIS geometry value, GiST-indexed), geom_wkt (a text copy of geom via ST_AsText), and srid (always 4326, WGS84 lon/lat). Storing geom_wkt next to geom keeps the geometry twice, in two formats, on every row.
Where that triad lives splits the schema in two:
| Table | Geometry kind | Storage pattern |
|---|---|---|
network_gis |
POINT | dedicated geometry table, FK network_id -> network.id (1:1, mirrored by network.has_gis) |
reservoir |
MULTIPOLYGON Z | separate table, soft-linked to reservoir_entity by short_code (no FK) |
wba |
POLYGON | inline on the entity row |
compliance_station |
POINT | inline on the entity row (also keeps latitude / longitude) |
du_urban_entity |
MULTIPOLYGON | inline on the entity row |
du_agriculture_entity |
MULTIPOLYGON | inline on the entity row |
du_refuge_entity |
MULTIPOLYGON | inline on the entity row |
Only network_gis and reservoir keep geometry in a table of its own. The other five (wba, compliance_station, and the three du_*_entity tables) carry the triad inline on the entity row, so the storage pattern is the same across all five. The demand-unit tables are the ones flagged for cleanup, for two reasons specific to them: their footprints are by far the largest (dissolved MultiPolygons, the cells that overflow the audit CSV export), and their entity rows are read constantly for non-spatial data (gw/sw flags, contractor links, statistics, tiers, the API), so the heavy geom is dead weight on a hot table. wba and compliance_station share the inline pattern but are smaller and rarely read for non-spatial work, so they are lower priority.
The three demand-unit entity tables carry the triad plus a catalog flag (added by 56_add_du_geometry_columns.sql):
| Column | Type | Notes |
|---|---|---|
geom |
geometry(MultiPolygon, 4326) |
dissolved DU footprint |
geom_wkt |
text |
WKT mirror of geom (ST_AsText(geom)) |
srid |
integer |
always 4326 |
has_gis_data |
boolean |
catalog flag, a polygon is expected for this du_id |
Each DU table also has a GiST index idx_<table>_geom. The columns were added in anticipation of serving polygons through the API for the get-started animation, but that path was never needed: the animation reads polygon coordinates straight from the Mapbox demand-units vector tiles (querySourceFeatures) and builds the SVGs from those (coeqwal-website apps/main/app/features/scenarioExplorer/animation/TierAnimationSection.tsx).
Geometry is latent at runtime. Nothing queries it spatially (no ST_Within / ST_Intersects / ST_Contains anywhere) and no live API or frontend reads it. PostGIS is used for offline geometry construction in the loader (load_du_geometries.py, a database script, not part of the statistics ETL), for ST_AsText / ST_AsGeoJSON serialization, and for storage plus GiST indexes.
Demand-unit geometry should move off the entity rows into a dedicated geometry table, the pattern network_gis and reservoir already use. The target:
- One dedicated DU geometry table keyed by
(du_id, du_class), preferred over three per-class tables becausedu_idis not unique across classes (26N_NAexists in both urban and ag). Mirror thereservoirDDL. - FK-enforce the link back to the entity rows, the way
network_gis.network_idreferencesnetwork.id(the cleanest existing example), rather than the soft, unenforced linkreservoiruses. - Drop the redundant
geom_wkttext mirror. Regenerate WKT on demand withST_AsTextif a consumer needs it. - Apply the same rule to CWS: when
cws_entitylands, put PWS points in a dedicated geometry table, never on thecws_entityattribute row.
Polygons are loaded by the database script load_du_geometries.py, which reads the geopackage seed_tables/03_GIS/du_4326.gpkg and matches on du_id (the column migration 56_add_du_geometry_columns.sql is already applied to RDS). This is a SQL / script load, not part of the statistics ETL. When agency-sourced polygons arrive, add them to the geopackage and rerun the loader.
Not every DU row has a polygon. From the May 2026 audit snapshot:
| Table | Rows | geom populated |
geom NULL |
|---|---|---|---|
du_urban_entity |
145 | 86 | 59 |
du_agriculture_entity |
144 | 132 | 12 |
du_refuge_entity |
18 | 17 | 1 |
| Combined (distinct) | 306 | 234 | 72 |
Distinct counts net out 26N_NA, the one du_id present in both the urban and ag tables. In this snapshot has_gis_data equals geom IS NOT NULL on every table (86, 132, 17), so the catalog flag and the actual geometry agree with no drift. To re-check current state:
SELECT 'du_urban_entity' AS table_name,
count(*) FILTER (WHERE geom IS NOT NULL) AS geom_not_null,
count(*) FILTER (WHERE has_gis_data) AS has_gis_data_true
FROM du_urban_entity
UNION ALL
SELECT 'du_agriculture_entity',
count(*) FILTER (WHERE geom IS NOT NULL),
count(*) FILTER (WHERE has_gis_data)
FROM du_agriculture_entity
UNION ALL
SELECT 'du_refuge_entity',
count(*) FILTER (WHERE geom IS NOT NULL),
count(*) FILTER (WHERE has_gis_data)
FROM du_refuge_entity;The du_ids with no polygon are in the appendix.
26N_NA is the only du_id present in both du_urban_entity and du_agriculture_entity. Both rows carry the same dissolved MULTIPOLYGON, so either table returns the same geometry for it.
tier_location (the tier map catalog) points at a few du_ids that have no row in the entity tables at all, so they have neither attributes nor geometry:
- AG_REV:
07S_PA - CWS_DEL:
ACFC,KCWA,MHILL_NU,SBCWD,SVWRD,TLMNE,UNION
These appear in tier results but cannot be drawn from DB geometry until the entity rows exist.
The target layout is in How geometry should be represented, tracked as SCHEMA_BACKLOG.md § 6f. Pickup steps:
- Decide the table layout (three tables vs one
demand_unit_geometrywith adu_classdiscriminator) and update the ERD. - Write the CREATE migration for the new geometry tables (mirror the
reservoirDDL style). Draft files58_create_du_geometry_tables.sqland59_migrate_du_geom_off_entity_tables.sql(neither is on disk today). - Refactor
load_du_geometries.pyto write the dedicated tables. - Update the
GeometryResolverfordemand_unitinetl/common/tier_location_entities.pyto point at the new tables. The resolver feeds the Mapbox tile-build pipeline. The API does not consume it for geometry. - Data migration: copy any existing rows from
du_*_entity.geominto the new tables. - Drop
geom,geom_wkt,srid, and the GiST index from the threedu_*_entitytables. - CWS rollout follow-on: when
cws_entitylands, put PWS points in a dedicated geometry table, not oncws_entityattribute rows. Same rule as DU.
The 72 du_ids with geom NULL in the May 2026 snapshot.
26N_NU513, 60N_PA, 60N_PU1, 60S_PA, 60S_PU, 61_PA, 61_PU1, 61_PU2, 63_PA, 63_PR, 64_PA, 64_PU, 65_PA, 65_PU, 70_PA, 70_PU1, 71_PA, 72_PU, 72_PU1, 90_PU5, AMADR, AMCYN, ANTOC, BNCIA, CCWD, CCWDI, CLLPT, CSB038, CSB103, CSPSO, CSTIC, CWD, EBMUD, ELDID_NU1, ELDID_NU2, ELDID_NU3, ESB324, ESB347, ESB414, ESB415, ESB420, FRFLD, GDPUD_NU, GRSVL, JLIND, MWD, NAPA, NAPA2, PCWA3, PINES, PLMAS, SBA029, SBA036, SCVWD, SUISN, TVAFB, UPANG, VLLJO, WLDWD
63_NA5, 91_PA, CULOS, CVP, GDPUD_NA, LCCWD, NIDBR_NA, NIDDC_NA1, NIDDC_NA2, NIDDC_NA3, PUTAH, SIDSH
91_PR