Southern California Edison · meter basemap

Field guide to the data

What each layer means, where it came from,
and how to query it without getting a wrong answer
Build state 3 September 2026
01

The model in one paragraph

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.

02

Where it came from

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.

SCE's own grid geography

The most important design decision in the project: SCE publishes its network, so results roll up along its geography rather than ours.

LayerRecordsWhat it gives
sce_ica_sections524,522ICA line sections — the allocation unit, and the granularity SCE planners work in
sce_distribution_circuits4,304Feeders. Circuit geometry now comes from dissolving the scored ICA sections, so all 3,005 circuits carrying meters have a shape
sce_substations735Projected load, remaining capacity, penetration — the phasing unit
sce_territory1SCE's authoritative service-territory polygon
sce_hftd5,336CPUC High Fire Threat District with cpuc_tier, from SCE's own hub

County parcels — all fifteen counties

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:

  • Tier A — dwelling-unit counts published directly. Los Angeles, Riverside.
  • Tier B — a use code or zoning string is present; units must be modelled from it.
  • Tier C — geometry and an assessor number only. Needs an assessor-roll join before it can be classed at all.
  • Tier D — no public service.
Correction against the original register

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.

Census geography — the second axis, and the validation control

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.

03

How a county endpoint becomes a scored line section

SCE + CPUC 5 grid layers, 535,000 features US Census 2020 blocks, places, CDPs County assessors 11 of 15 counties publish paginated REST, 1,000–2,000 features per request pipeline/extract.py one extractor for every endpoint — they are all the same API stamps source_url + retrieved_at on every row 8,637,423 features · 0 count drift against the register data/interim/<name>.fgb source schema unmodified · companion .manifest.json records served vs assembled pipeline/territory.py erase 21 publicly owned utilities, keep 3 135,380 − 3,924 = 131,456 km² pipeline/clip.py parcel representative point inside territory 6,478,429 → 4,133,590 (63.8%) net boundary data/core/<county>_sce.parquet clipped parcels, EPSG:3310, unique parcel_uid geometry only attributes only STAGE 1 — WHERE pipeline/allocate.py <county>_parcel_alloc.parquet one row per (parcel, block) · complete STAGE 2 — HOW MANY pipeline/units.py <county>_units.parquet one row per parcel · all 15 counties join on parcel_uid core.service_point one row per metered service · the only true atom every rollup is a GROUP BY over this marts.line_section_profile 524,522 scored sections · difficulty index, cost multiplier PMTiles web map — network live GeoParquet + CSV index File geodatabase
Solid outlines are built and measured; dashed outlines are specified but not yet written. The split after clipping is the load-bearing design decision: stage 1 depends only on parcel geometry, stage 2 only on parcel attributes. The units model gets revised all through calibration and parcel geometry does not, so a crosswalk change re-runs a cheap non-spatial join instead of a four-million-parcel spatial one.

Why the territory carve-out is opt-in, not a blanket difference

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.

The largest trap in the CEC layer

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.

04

Two aggregation axes, and why neither derives the other

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.

GRID AXIS — HOW SCE PLANS CENSUS AXIS — HOW IT IS VALIDATED substation_name 735 · the phasing unit circuit_name 4,304 feeders section_id 524,522 · crews dispatch here sections nest into circuits circuits nest into substations — both cached on the row county — geoid[:5] tract_geoid — geoid[:11] bg_geoid — geoid[:12] block_geoid 15 chars · carries HU100 control string truncation, not a join string truncation string truncation these two do not nest a block straddles many sections; a circuit crosses many blocks parcel → service point the only true atom · EPSG:3310 place_geoid the CA DOF E-5 calibration key crosswalk, not truncation — places carry no code in the GEOID
Both keys must sit on the parcel row from the start. Neither rollup can be derived from the other, and retrofitting the second key later means re-running allocation across four million parcels. 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.
05

The two ambiguities are different problems

Blocks tessellate → split a parcel genuinely spanning two blocks belongs to both block A block B parcel block_share 0.6 block_share 0.4 shares sum to 1.0 per parcel — a pipeline gate, not a nicety today every share is 1.0; see the caption Sections are lines → never split one true answer exists; it is merely unobserved section 1 · circuit A section 2 · circuit B nearest · 79 m runner-up · 88 m dist_ratio 0.90 alt_circuit_differs flagged for review only when both are true 7.5% of LA, 6.3% of Riverside — a queue someone can work
A confidence flag has to be defined at the grain of the decision it informs, not the grain that is easiest to compute. The distance ratio alone was supposed to mark parcels sitting between two feeders. Measured on Los Angeles its median is 0.924, and a > 0.8 threshold flags 70% of the county — because in a dense network the two nearest sections are nearly equidistant by construction. Two candidates on the same circuit give the same circuit and the same substation, so the ambiguity changes no downstream answer. Only a runner-up on a different circuit is worth a reviewer's time.
Why block_share is 1.0 everywhere today

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.

06

The files on disk, and what each column means

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.

<county>_sce.parquet — clipped parcels

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.

ColumnMeaning
parcel_uidCounty FIPS + assessor number. The join key. Unique per county — enforced as a gate
apn_rawThe assessor number verbatim, kept for the assessor-roll join
uid_disambiguatedTrue where the source number was not unique and a #n suffix was added
county_fipsFive-character county FIPS
source_url, retrieved_atProvenance, on every row
geometryParcel polygon, EPSG:3310
Assessor numbers are not primary keys

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.

<county>_parcel_alloc.parquet — stage 1, where the parcel is

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.

ColumnMeaning
block_geoid15 characters. Truncate for the other levels — do not store or re-join them
block_shareShare of this parcel's meters in this block. Weight by this in every rollup
blocks_touchedHow many blocks the polygon intersects. QA measure of how wrong the 1.0 share is
place_geoid, place_name, place_typeIncorporated place, else CDP, else null for unincorporated
section_id, node_idNearest ICA line section. Never split across sections
circuit_name, substation_name, circuit_voltageDenormalised off the section row, never joined independently
section_dist_mDistance to that section. No maximum snap distance is applied
section_dist_ratioNearest ÷ second-nearest. Near 1 means the two are equidistant
alt_circuit_nameThe runner-up section's circuit
alt_circuit_differs, alt_substation_differsWhether the ambiguity survives the rollup — the flag that matters
geometryParcel representative point, EPSG:3310 — not the polygon

<county>_units.parquet — stage 2, how many meters it holds

One row per parcel. Currently built for Los Angeles, Riverside parcels and Riverside condominium polygons.

ColumnMeaning
code_rawThe source use code, verbatim, never normalised. Audit trail for the crosswalk
property_classOne of fifteen normalised classes, from config/crosswalk.csv
dwelling_unitsEstimated dwelling units. Not meters
units_methodmeasured inferred modelled
units_sourceWhich rung produced it: assessor unit count, crosswalk rate, or class constant, no published count
meters_estServices implied, per config/meters.yml
meters_methodInherited from the weakest input — a modelled rate on a measured unit count is still modelled
year_builtWhere published. The panel-vintage proxy for the difficulty index

The conversion from units to meters

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.

07

How to query it

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.

Meters per line section, rolled up to circuit

# 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()

The same meters on the census axis, with the independent control

# 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()

Geometry, when you actually need it

# 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

Four rules that keep the answer right

RuleWhy
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.
Never report a whole-county figure as an SCE figure

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.

08

Published surfaces

ArtifactStateNotes
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.