A parcel is not a meter. This dataset exists to get from the thing counties publish — a polygon with an assessor's use code — to the thing SCE dispatches crews against: a metered service connection sitting on one line section, in one census block, with a defensible statement of how confidently we know both.
Every number here is therefore two numbers: an estimate and a label saying how it was derived. The labels are measured, inferred and modelled, they are reported separately, and they are never averaged. A meter total without its method mix is not a usable figure in this project.
Two files carry every judgement call, and neither is code: config/crosswalk.csv says what a parcel is, and config/meters.yml says how many electric services its dwelling units imply. They are kept apart on purpose — a reviewer can accept the classification and still argue with the conversion, which is the argument worth having.
Thirty sources, all harvested live and all ArcGIS REST, totalling 8,637,423 assembled features with zero count drift against the register. Every harvested row carries source_url and retrieved_at; every harvest writes a companion *.manifest.json recording what the service claimed to hold versus what actually arrived. Provenance is not an afterthought in a bid deliverable, and a hand-maintained source list would already be wrong.
The most important design decision in the project: SCE publishes its network, so results roll up along its geography rather than ours.
| Layer | Records | What it gives |
|---|---|---|
| sce_ica_sections | 524,522 | ICA line sections — the allocation unit, and the granularity SCE planners work in |
| sce_distribution_circuits | 4,304 | Feeders. Circuit geometry now comes from dissolving the scored ICA sections, so all 3,005 circuits carrying meters have a shape |
| sce_substations | 735 | Projected load, remaining capacity, penetration — the phasing unit |
| sce_territory | 1 | SCE's authoritative service-territory polygon |
| sce_hftd | 5,336 | CPUC High Fire Threat District with cpuc_tier, from SCE's own hub |
Fresno, Kings, Madera and Inyo publish no parcel service and are resolved by direct assessor records request, not by scraping third-party mirrors: provenance that cannot be cited is worthless in an RFP. All four are minority-share SCE territory, so they are the cheapest to lose.
Counties are graded by distance from the published layer to a per-parcel meter estimate, not by data quality in the abstract:
Orange County was registered as Tier C, “geometry and APN only”. It in fact publishes YEAR_BUILT on 515,812 parcels and NBR_BEDROOMS on 466,661 — the panel-vintage proxy the difficulty index needs, plus a real dwelling signal. It has no use code, so it still cannot be classed, but it is Tier B on evidence. Read the layer before trusting any description of it, including this one.
270,639 blocks, 482 places and 1,129 CDPs across the fifteen counties, all matching expected counts exactly. Only blocks are pulled: the 15-character block GEOID nests exactly, so block group is the first 12 characters, tract the first 11 and county the first 5. One spatial join yields three levels by string truncation, and truncation cannot disagree with itself the way three separate joins can.
TIGERweb's block layer carries HU100 and POP100 itself, so the 2020 decennial housing-unit control arrives with the geometry — the independent per-block check the calibration gate needs, with no separate Census API pull. Across all fifteen counties, unclipped: 7,750,632 housing units and 22,060,189 people. That is the frame the 5.7M meter figure sits inside, not a number to compare it against; roughly a third of those counties belongs to a utility other than SCE.
SCE's own polygon already excludes the large historic municipal holes — Glendale and Burbank do not overlap it at all, Pasadena 0.9%, Anaheim 5.7%. What it does not remove is a shorter list of newer publicly owned utilities sitting entirely inside it: Moreno Valley, City of Industry, Cerritos, Victorville, Corona, Rancho Cucamonga, plus Needles and Bear Valley Electric.
Metropolitan Water District is the single biggest overlap with SCE at 7,710 km², which makes it look like the most important thing to erase. It is not one. MWD is a wholesale water agency with no retail electric meters, listed by the CEC only because it serves its own pumping load. Erasing it would delete 5.7% of SCE's territory, on the populated coastal plain. Power and Water Resource Pooling Authority is the same class of mistake.
This is why config/territory.yml lists each utility with a recorded reason and a keep section for the near misses — so a reviewer can see what was considered, not just what was removed.
Meters must roll up both along SCE's network and along standard census geography. They answer different questions and neither substitutes for the other: the grid axis prices deployment and phases the work, the census axis supplies the independent housing-unit control and the equity reporting the CPUC requires.
place_geoid is the exception that proves the rule: city does not fall out of the block GEOID and many places straddle county lines, so it needs its own crosswalk — but without it the calibration gate against CA Department of Finance E-5 cannot run at all.The one source that says where a meter sits rather than how many exist — national address points — is unavailable, so every service point is synthesised at its parcel's representative point and all of a parcel's meters land in a single block. The column exists anyway, and blocks_touched measures how wrong that is: Los Angeles 9.3%, Kern 19.1%, Tuolumne 33.3%, Imperial 39.4% of parcels touch more than one block. That is the size of the correction waiting, per county. When address points land the rows multiply, the shares stop being 1.0, and nothing downstream is re-plumbed.
Everything is per-county Parquet under data/core/, in three families that join on parcel_uid. Geometry is GeoParquet 1.0 WKB in EPSG:3310, California Albers — all area, length and distance maths happens there, and reprojection to 4326 or 3857 happens only at export. Areas computed in Web Mercator are wrong and this project never does it.
The source layer's own fields, lowercased and otherwise untouched, plus the keys the project adds. Raw use codes and zoning strings are preserved forever alongside normalised values: every reviewer will want to audit a crosswalk decision.
| Column | Meaning |
|---|---|
| parcel_uid | County FIPS + assessor number. The join key. Unique per county — enforced as a gate |
| apn_raw | The assessor number verbatim, kept for the assessor-roll join |
| uid_disambiguated | True where the source number was not unique and a #n suffix was added |
| county_fips | Five-character county FIPS |
| source_url, retrieved_at | Provenance, on every row |
| geometry | Parcel polygon, EPSG:3310 |
Orange County has 688,020 distinct ASSESSMENT_NO over 702,986 rows: 4,277 repeat across 18,367 rows, one of them 337 times; 1,795 carry the sentinel 000 and 876 are null. Once the uniqueness pass ran everywhere it caught roughly 42,000 parcels whose identifier was not unique — San Bernardino 18,715, Orange 15,461, Ventura 7,286, Los Angeles 112. Only Kern and Tuolumne were clean.
A duplicated key is worse than an ugly one: every downstream join fans out silently and the meter count inflates with no error raised. If you join on parcel_uid, check that your row count did not grow.
One row per (parcel, block), not per parcel. Complete for all fifteen counties: 4,133,590 rows, zero parcels failed to find a line section.
| Column | Meaning |
|---|---|
| block_geoid | 15 characters. Truncate for the other levels — do not store or re-join them |
| block_share | Share of this parcel's meters in this block. Weight by this in every rollup |
| blocks_touched | How many blocks the polygon intersects. QA measure of how wrong the 1.0 share is |
| place_geoid, place_name, place_type | Incorporated place, else CDP, else null for unincorporated |
| section_id, node_id | Nearest ICA line section. Never split across sections |
| circuit_name, substation_name, circuit_voltage | Denormalised off the section row, never joined independently |
| section_dist_m | Distance to that section. No maximum snap distance is applied |
| section_dist_ratio | Nearest ÷ second-nearest. Near 1 means the two are equidistant |
| alt_circuit_name | The runner-up section's circuit |
| alt_circuit_differs, alt_substation_differs | Whether the ambiguity survives the rollup — the flag that matters |
| geometry | Parcel representative point, EPSG:3310 — not the polygon |
One row per parcel. Currently built for Los Angeles, Riverside parcels and Riverside condominium polygons.
| Column | Meaning |
|---|---|
| code_raw | The source use code, verbatim, never normalised. Audit trail for the crosswalk |
| property_class | One of fifteen normalised classes, from config/crosswalk.csv |
| dwelling_units | Estimated dwelling units. Not meters |
| units_method | measured inferred modelled |
| units_source | Which rung produced it: assessor unit count, crosswalk rate, or class constant, no published count |
| meters_est | Services implied, per config/meters.yml |
| meters_method | Inherited from the weakest input — a modelled rate on a measured unit count is still modelled |
| year_built | Where published. The panel-vintage proxy for the difficulty index |
meters = dwelling_units × per_unit × (1 − master_metered_share) + per_parcel
Every constant in that formula lives in config/meters.yml with a written rationale, because none of them can be verified from public data. Two of the rows exist specifically to avoid a trap the assessor data walks you into: hotels and institutions report rooms and beds as “units”, so both carry per_unit: 0. Los Angeles alone reports 52,782 hotel “units” across 1,270 parcels; a 200-room hotel is one service, not 200. Deleting that one line inflates the county by fifty thousand meters.
Nothing needs to be online. The rollups are non-spatial joins over Parquet — the spatial work is already done and cached in stage 1 — so DuckDB against the files is the fastest route to an answer.
# the canonical rollup: weight by block_share, never sum from a modal key
import duckdb
duckdb.sql("""
SELECT a.circuit_name,
a.substation_name,
sum(u.meters_est * a.block_share) AS meters,
sum(u.meters_est * a.block_share)
FILTER (WHERE u.meters_method = 'measured') AS measured,
count(DISTINCT a.section_id) AS sections
FROM 'data/core/la_parcels_parcel_alloc.parquet' a
JOIN 'data/core/la_parcels_units.parquet' u USING (parcel_uid)
GROUP BY 1, 2
ORDER BY meters DESC
""").show()
# three geography levels from one column, by truncation
duckdb.sql("""
SELECT substr(a.block_geoid, 1, 11) AS tract_geoid,
sum(u.meters_est * a.block_share) AS meters,
sum(u.dwelling_units * a.block_share) AS units
FROM 'data/core/la_parcels_parcel_alloc.parquet' a
JOIN 'data/core/la_parcels_units.parquet' u USING (parcel_uid)
GROUP BY 1
""").show()
# GeoParquet 1.0, EPSG:3310 — geopandas reads the CRS from the file import geopandas as gpd pts = gpd.read_parquet("data/core/la_parcels_parcel_alloc.parquet") # points poly = gpd.read_parquet("data/core/la_parcels_sce.parquet") # polygons
| Rule | Why |
|---|---|
Always weight by block_share |
It is 1.0 today, so omitting it is currently harmless — and silently wrong the day address points land. Write it now. |
| Never report a bare meter total | Carry the method mix with it. Rendering a 90%-modelled block in the same colour ramp as a 90%-measured one gives modelled figures the authority of measured ones. |
| Assert the invariant | Total meters by block must equal total by line section must equal the grand total. Splitting across blocks while assigning whole to sections is exactly the shape of error that double-counts, and this equality is what catches it. |
LEFT JOIN when materialising polygon counts |
So zero-meter blocks and vacant parcels render as zero rather than disappearing into holes that read as missing data. |
Clipping to territory removes 32.1% of parcels overall, and the per-county spread is why a blended ratio is never acceptable: Ventura keeps 99.9%, San Bernardino 93.9%, Los Angeles 60.8%, Tulare 34.8% — and Imperial and Tuolumne keep 0.6% each. Los Angeles alone sheds 953,478 parcels to LADWP and the other municipal utilities.
| Artifact | State | Notes |
|---|---|---|
| data/out/sce_cube.pmtiles | live | 157 MB, seven layers: census tract, block group and block, plus substation, circuit at two resolutions and ICA line section. Every level carries the same attribute names, so one cost model evaluates against all of them. Read straight by MapLibre over HTTP range requests — no tile server, no database online. |
| data/out/basemap_socal.pmtiles | live | 183 MB, Protomaps planet build 20260901 clipped to the territory polygon buffered 8 km. Self-hosted so the map’s appearance does not depend on a third-party tile service. |
| data/out/sce_index.pmtiles | live | 62 MB, the difficulty index at section, circuit and substation grain, plus SCE’s published network and HFTD tiers. |
| sce.weleber.net | live | nginx on an Oracle A1 host, TLS auto-renewing. The bucket copy stays populated as a second live URL so a deliverable under review has no single point of failure. |
| Parcel tiles | live | 4,133,590 parcels, zoom 13–16, each carrying its estimated service points, install class, dwelling units and build year. Published under the CPRA: Sierra Club v. Superior Court (Orange County) (2013) held a county parcel base map to be a public record, disclosable at the cost of duplication, and that access may not be conditioned on a restrictive licence. Citation is the condition — each county is named as author with its endpoint and retrieval date in CITATION.md. An earlier draft of this page withheld the layer on the opposite reading; that was wrong. |
| Line-section index — GeoParquet, CSV, FGDB | live | 272,354 scored sections plus circuit, substation and territory layers, as GeoParquet and CSV and as four layers of an OpenFileGDB. The Parquet is HTTP range-readable, so DuckDB answers a GROUP BY over every section in under a second without downloading the 46 MB file. catalog.json lists them with checksums and a column glossary. |
| Deployment cost model | live | Forty-one controls, a choropleth over seven geographies down to the parcel, a cost waterfall, a schedule with a deadline-to-crews inversion and a sensitivity ranking. Runs entirely in the browser against a pre-aggregated cube. |
| MCP endpoint | live | Read-only Model Context Protocol server at /mcp — catalogue, schema, headline figures, the cost-model config and read-only SQL over the published tables. |
Anything baked into a tile is filterable client-side with nothing running behind the map; anything omitted needs a database. config/tiles.yml keeps the field allowlist deliberately short for that reason, and excludes provenance columns — they are per-layer constants and belong in the archive description, not repeated on every feature in every tile.