Geo

Geospatial: the gravitational center. Boundaries, geocoding, spatial transforms, Census data intelligence, isochrones, redistricting analysis, CRS management, GEOID utilities, interpolation, gazetteers, place history, overlays.

Geographic utilities for spatial data analysis, Census data access, and mapping.

All submodules load on first attribute access via PEP 562 __getattr__. Pure-Python modules (census_constants, geoid_utils) are importable without geopandas.

siege_utilities.geo.geo_capabilities()[source]

Detect available spatial packages and report the active tier.

Returns a dict with boolean flags for each optional package and a tier string ("geodjango", "geo", "geo-lite", or "none").

>>> caps = geo_capabilities()
>>> caps["tier"] in ("geodjango", "geo", "geo-lite", "none")
True
>>> isinstance(caps["shapely"], bool)
True
Return type:

Dict[str, Any]

siege_utilities.geo.get_default_crs()[source]

Return the current default CRS string (e.g. "EPSG:4326").

Return type:

str

siege_utilities.geo.set_default_crs(crs)[source]

Set the default CRS used by all spatial-returning functions.

Parameters:

crs (str) – Any CRS string accepted by pyproj (e.g. "EPSG:4326", "EPSG:3857", "ESRI:102003").

Raises:

pyproj.exceptions.CRSError – If crs is not recognized by pyproj.

Return type:

None

siege_utilities.geo.reproject_if_needed(gdf, crs=None)[source]

Reproject gdf to crs if it differs from the GeoDataFrame’s current CRS.

If crs is None, uses get_default_crs(). Returns the GeoDataFrame unchanged if it is already in crs or is empty.

When the GeoDataFrame has no CRS (gdf.crs is None) and a target is specified, the target CRS is set (not reprojected) on the assumption that the data is already in the target CRS. A warning is logged because this assumption may be wrong.

Parameters:

crs (str | None)

siege_utilities.geo.detect_crs(geom_or_ds)[source]

Resolve the source CRS of a spatial object.

Parameters:

geom_or_ds (Any) –

One of:

  • A GeoDataFrame — reads .crs and returns an EPSG:<n> string when authoritative, otherwise the WKT.

  • A GeoJSON-like dict — reads ["crs"]["properties"]["name"] if present.

  • A fiona Collection — reads .crs / .crs_wkt.

  • A bare shapely geometry — bare geometries carry no CRS metadata; this returns None (callers should treat this as “consult get_default_crs()”).

  • None — returns None.

Returns:

An EPSG string ("EPSG:4326"), a WKT string, or None.

Return type:

str | None

Mirrors the SRS-detection branch of the salvage source (ImportServerRepo.py:250-253), which read the spatial reference from an OGR DataSource via GetGeometryRef().GetSpatialReference(). This implementation reads .crs / .crs_wkt from GeoDataFrames or fiona collections — same intent, declared-deps surface.

siege_utilities.geo.distance_to_decimal_degrees(distance_meters, latitude)[source]

Convert a distance in meters to approximate decimal degrees.

Uses the WGS-84 ellipsoid approximation. The result is the longitudinal span at the given latitude (degrees of longitude shrink toward the poles).

Parameters:
  • distance_meters (float) – Distance in meters.

  • latitude (float) – Latitude in decimal degrees where the conversion applies.

Returns:

Approximate equivalent distance in decimal degrees of longitude at latitude.

Raises:

ValueError – If latitude is outside [-90, 90].

Return type:

float

siege_utilities.geo.reproject_geom(geom, src_crs, dst_crs=4326, axis_order='trad_gis')[source]

Reproject a shapely geometry with explicit axis-order control.

Parameters:
  • geom – A shapely geometry (Point, LineString, Polygon, etc.) or None.

  • src_crs (Any) – The geometry’s source CRS, in any form pyproj.CRS.from_user_input accepts ("EPSG:4326", an integer EPSG code, a WKT string, a pyproj.CRS object).

  • dst_crs (Any) – Target CRS, in any form pyproj.CRS.from_user_input accepts. Defaults to 4326 (WGS 84).

  • axis_order (str) –

    One of:

    • "trad_gis" (default) — pyproj always_xy=True. Coordinates are interpreted and emitted as (lon, lat), matching GeoJSON, Shapefile, KML, and most consumer expectations. This is the modern-pyproj equivalent of osgeo.osr.OAMS_TRADITIONAL_GIS_ORDER used in the salvage source (ImportServerRepo.py:267-268).

    • "auth_compliant"always_xy=False. Coordinates follow the CRS authority’s declared axis order (lat-first for EPSG:4326). Use only if you know your input data is lat-first and your consumer expects lat-first.

Returns:

A new shapely geometry in the target CRS, or None if geom is None.

Raises:
  • ValueError – If axis_order is not one of the two accepted tokens, or if src_crs is None (a CRS-less geometry cannot be reprojected without an explicit source).

  • ImportError – If pyproj or shapely are not installed.

Round-trip note: 4326 -> 3857 -> 4326 on a Point should return the same coordinates within 1e-6 degrees. The test suite verifies this.

class siege_utilities.geo.BoundaryFetchResult[source]

Bases: object

Structured result from a boundary retrieval attempt.

success

Whether the retrieval succeeded.

Type:

bool

geodataframe

The resulting GeoDataFrame (None on failure).

Type:

Any

error_code

Machine-readable error code (None on success).

Type:

str | None

error_stage

Pipeline stage where failure occurred (None on success). One of: input_validation, discovery, url_validation, download, parse.

Type:

str | None

message

Human-readable description of the outcome.

Type:

str

context

Diagnostic details (attempted URLs, year fallback, HTTP status, etc.).

Type:

Dict[str, Any]

success: bool
geodataframe: Any = None
error_code: str | None = None
error_stage: str | None = None
message: str = ''
context: Dict[str, Any]
raise_on_error()[source]

Raise the appropriate typed exception if this result is a failure.

Returns self on success so callers can chain:

gdf = fetch_geographic_boundaries(...).raise_on_error().geodataframe
Return type:

BoundaryFetchResult

classmethod ok(gdf, message='', context=None)[source]

Construct a success result.

Parameters:
Return type:

BoundaryFetchResult

classmethod fail(error_code, error_stage, message, context=None)[source]

Construct a failure result.

Parameters:
Return type:

BoundaryFetchResult

__init__(success, geodataframe=None, error_code=None, error_stage=None, message='', context=<factory>)
Parameters:
Return type:

None

exception siege_utilities.geo.BoundaryRetrievalError[source]

Bases: SiegeGeoError

Base exception for all boundary retrieval failures.

Inherits from SiegeGeoError so callers can catch the entire siege_utilities exception family with a single except SiegeError:. Previously this stood alone outside the documented hierarchy and slipped past except SiegeError blocks.

__init__(message, stage, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryInputError[source]

Bases: BoundaryRetrievalError

Invalid input parameters (state FIPS, year, geographic level).

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryDiscoveryError[source]

Bases: BoundaryRetrievalError

Failed to discover available boundary types or construct a URL.

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryConfigurationError[source]

Bases: BoundaryRetrievalError

Boundary type requires parameters that were not provided (e.g., state FIPS, congress number).

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryUrlValidationError[source]

Bases: BoundaryRetrievalError

Constructed URL is not accessible (HTTP error, timeout, etc.).

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryDownloadError[source]

Bases: BoundaryRetrievalError

Download succeeded but the file is corrupt or not a valid zip.

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.BoundaryParseError[source]

Bases: BoundaryRetrievalError

Downloaded data could not be parsed as a shapefile/GeoDataFrame.

__init__(message, context=None)[source]
Parameters:
class siege_utilities.geo.BoundaryProvider[source]

Bases: ABC

Abstract base class for geographic boundary data providers.

abstract property provider_name: str

Human-readable name for this provider.

abstractmethod get_boundary(level, identifier=None, **kwargs)[source]

Fetch boundary geometry for a given geographic level.

Parameters:
  • level (str) – Geographic level (e.g. ‘county’, ‘tract’, ‘admin1’).

  • identifier (str | None) – Optional identifier to narrow the query (e.g. state FIPS).

  • **kwargs (Any) –

    Provider-specific options. Recognized cross-provider kwargs: engine: Optional DataFrameEngine instance. When provided,

    the result is converted to the engine’s native format via engine.from_geodataframe(). Default (None) returns a GeoDataFrame.

Returns:

GeoDataFrame when engine is None; engine-native DataFrame otherwise.

Return type:

Any

abstractmethod list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

abstractmethod is_available()[source]

Return True if this provider’s dependencies are installed and reachable.

Return type:

bool

class siege_utilities.geo.CensusTIGERProvider[source]

Bases: BoundaryProvider

US Census TIGER/Line boundary provider.

Wraps siege_utilities.geo.spatial_data.CensusDataSource and siege_utilities.config.census_constants.CANONICAL_GEOGRAPHIC_LEVELS.

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch US Census TIGER boundaries.

Parameters:
  • level (str) – A canonical geographic level (e.g. ‘county’, ‘tract’, ‘cd’).

  • identifier (str | None) – State FIPS code when the level requires it.

  • **kwargs (Any) – Forwarded to CensusDataSource.fetch_geographic_boundaries (e.g. year, congress_number). engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:

BoundaryFetchError – When the boundary retrieval fails.

list_levels()[source]

Return canonical Census geographic level names.

Return type:

list[str]

is_available()[source]

Census TIGER provider is always available (pure-HTTP downloads).

Return type:

bool

list_available_vintages(timeout=30)[source]

Fetch the Census TIGER listing and parse out published TIGER{YYYY} years.

Returns a sorted list of int years.

Raises:

requests.RequestException – On network failure.

Parameters:

timeout (int)

Return type:

list[int]

latest_vintage(timeout=30)[source]

Return the newest published TIGER vintage year, or None on failure.

Parameters:

timeout (int)

Return type:

int | None

class siege_utilities.geo.GADMProvider[source]

Bases: BoundaryProvider

GADM (Global Administrative Areas) boundary provider.

Downloads GeoJSON boundary files from the GADM project for non-US countries. Requires geopandas at runtime.

__init__(version='4.1')[source]
Parameters:

version (str)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch GADM boundaries for a country.

Parameters:
  • level (str) – One of ‘country’, ‘admin1’, ‘admin2’, ‘admin3’.

  • identifier (str | None) – ISO-3 country code (e.g. ‘DEU’, ‘FRA’). Can also be passed as country_code kwarg.

  • **kwargs (Any) – country_code accepted as an alias for identifier. engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:
  • ValueError – If level is unknown or country_code is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return GADM administrative levels.

Return type:

list[str]

is_available()[source]

Return True if geopandas is importable.

Return type:

bool

class siege_utilities.geo.RDHProvider[source]

Bases: BoundaryProvider

Redistricting Data Hub boundary provider.

Wraps siege_utilities.geo.providers.redistricting_data_hub.RDHClient to expose precinct / VTD boundaries — and enacted legislative plans — through the standard BoundaryProvider interface.

RDH requires a “designated API user” account. Contact info@redistrictingdatahub.org to request access. Pass credentials either via constructor arguments or environment variables RDH_USERNAME / RDH_PASSWORD.

Supported levels

  • 'precinct' — precinct/VTD boundaries with election results

  • 'congress' — enacted congressional district plans

  • 'state_senate' — enacted upper-chamber plans

  • 'state_house' — enacted lower-chamber plans

Usage:

import os
from siege_utilities.geo.providers.boundary_providers import RDHProvider

# Prefer env vars (RDH_USERNAME / RDH_PASSWORD); never hardcode
provider = RDHProvider(
    username=os.environ["RDH_USERNAME"],
    password=os.environ["RDH_PASSWORD"],
)
gdf = provider.get_boundary('precinct', identifier='TX')
__init__(username=None, password=None, cache_dir=None)[source]
Parameters:
  • username (str | None)

  • password (str | None)

  • cache_dir (str | None)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch RDH boundary data for a state.

Parameters:
  • level (str) – One of 'precinct', 'congress', 'state_senate', 'state_house'.

  • identifier (str | None) – Two-letter state abbreviation (e.g. 'TX'). Can also be passed as state kwarg.

  • **kwargs (Any) – state — alias for identifier. year — filter datasets by year string (e.g. '2022'). format'shp' (default) or 'csv'. engine — Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided. None if no matching datasets found.

Raises:
  • ValueError – If level is not recognised or state is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

is_available()[source]

Return True if geopandas is installed and both credentials are set.

Return type:

bool

exception siege_utilities.geo.BoundaryFetchError[source]

Bases: RuntimeError

Raised when a boundary provider cannot satisfy a request after retries.

siege_utilities.geo.resolve_boundary_provider(country='US', **kwargs)[source]

Return an appropriate BoundaryProvider for the given country.

Parameters:
  • country (str) – ISO-2 or ISO-3 country code (default 'US').

  • **kwargs (Any) – Forwarded to GADMProvider constructor for non-US countries. Ignored for US (CensusTIGERProvider takes no options).

Returns:

CensusTIGERProvider for US / US territories, GADMProvider otherwise.

Return type:

BoundaryProvider

class siege_utilities.geo.CensusDirectoryDiscovery[source]

Bases: object

Discovers available Census TIGER/Line data dynamically.

__init__(timeout=None)[source]
Parameters:

timeout (int | None)

get_available_years(force_refresh=False)[source]

Get list of available Census years with retry and exponential backoff.

Parameters:

force_refresh (bool)

Return type:

List[int]

get_year_directory_contents(year, force_refresh=False, on_error='skip')[source]

Get contents of a specific TIGER year directory.

Parameters:
  • year (int) – Census TIGER year.

  • force_refresh (bool) – Bypass the in-memory cache.

  • on_error ("raise" | "warn" | "skip") – What to do when the network request fails. "skip" (default) returns an empty list silently; "warn" logs and returns empty; "raise" propagates the SiegeGeoError.

Return type:

List[str]

discover_boundary_types(year)[source]

Discover what boundary types are available for a given year.

Parameters:

year (int)

Return type:

Dict[str, str]

get_year_specific_url_patterns(year)[source]

Get year-specific URL patterns and directory structures.

Parameters:

year (int)

Return type:

Dict[str, Any]

construct_download_url(year, boundary_type, state_fips=None, congress_number=None)[source]

Construct download URL using FIPS validation and year-specific patterns.

Raises:
Parameters:
  • year (int)

  • boundary_type (str)

  • state_fips (str | None)

  • congress_number (int | None)

Return type:

str

validate_download_url(url)[source]

Check if a download URL is actually accessible.

Uses GET with stream=True instead of HEAD because CDNs like Cloudflare often block or throttle HEAD requests from non-browser User-Agents.

Raises:

BoundaryUrlValidationError – If the URL is not accessible.

Parameters:

url (str)

Return type:

bool

get_optimal_year(requested_year, boundary_type)[source]

Find the best available year for a requested boundary type.

Parameters:
  • requested_year (int)

  • boundary_type (str)

Return type:

int

class siege_utilities.geo.CensusDataSource[source]

Bases: SpatialDataSource

Census TIGER/Line boundary data source with dynamic year discovery.

Supports all standard TIGER/Line boundary types:

National-scope (no state_fips required):

nation, state, county, place, zcta, cd

State-scope (state_fips required):

tract, block_group, block, tabblock20, sldu, sldl

GEOID formats by boundary type:

Boundary

Digits

Components

state

2

SS

county

5

SS + CCC

tract

11

SS + CCC + TTTTTT

block_group

12

SS + CCC + TTTTTT + G

tabblock20

15

SS + CCC + TTTTTT + BBBB

cd

4

SS + DD

sldu

5

SS + DDD

sldl

5

SS + DDD

__init__(api_key=None, timeout=None)[source]

Initialize spatial data source.

Parameters:
  • name – Name of the data source

  • base_url – Base URL for the data source

  • api_key (str | None) – API key if required

  • timeout (int | None)

get_available_years(force_refresh=False)[source]

Get list of available Census years.

Parameters:

force_refresh (bool) – If True, re-discover years from Census server

Returns:

List of available years

Return type:

List[int]

get_year_directory_contents(year)[source]

Get directory contents for a specific year.

Parameters:

year (int)

Return type:

List[str]

discover_boundary_types(year)[source]

Discover available boundary types for a year.

Returns a mapping of boundary-type slug ('county', 'tract', …) to the canonical TIGER directory name ('COUNTY', 'TRACT', …). Matches the return contract of TigerDiscovery.discover_boundary_types(). The previous List[str] annotation was incorrect – the underlying call has always returned the dict.

Parameters:

year (int)

Return type:

Dict[str, str]

get_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, county_fips=None, congress_number=None)[source]

Download geographic boundaries from Census TIGER/Line files with dynamic discovery.

This method preserves the legacy Optional[GeoDataFrame] return type. For structured diagnostics, use fetch_geographic_boundaries() instead.

Parameters:
  • year (int) – Census year (will find closest available if not available)

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code for filtering (required for some levels)

  • county_fips (str | None) – County FIPS code for filtering

  • congress_number (int | None) – Congress number for congressional districts

Returns:

GeoDataFrame with boundaries or None if failed

Return type:

geopandas.GeoDataFrame | None

fetch_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, county_fips=None, congress_number=None)[source]

Download geographic boundaries with structured diagnostics.

Returns a BoundaryFetchResult that carries either the GeoDataFrame on success or a machine-readable error code, stage, and context dict on failure.

Parameters:
  • year (int) – Census year (will find closest available if not available)

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code for filtering (required for some levels)

  • county_fips (str | None) – County FIPS code for filtering

  • congress_number (int | None) – Congress number for congressional districts

Returns:

BoundaryFetchResult with .success, .geodataframe, .error_stage, etc.

Return type:

BoundaryFetchResult

get_available_boundary_types(year)[source]

Get available boundary types for a specific year.

Parameters:

year (int)

Return type:

Dict[str, str]

refresh_discovery_cache()[source]

Refresh the discovery cache to get latest available data.

get_available_state_fips()[source]

Get mapping of state FIPS codes to state names.

Return type:

Dict[str, str]

get_state_abbreviations()[source]

Get mapping of state FIPS codes to state abbreviations.

Return type:

Dict[str, str]

normalize_state_identifier(state_input)[source]

Convert any state identifier (FIPS, abbreviation, name) to FIPS code.

Deprecated since version 3.16.0: Use the module-level normalize_state_identifier() directly.

Raises:

ValueError – If the input cannot be resolved to a valid state FIPS.

Return type:

str

get_comprehensive_state_info()[source]

Get comprehensive state information including FIPS, name, and abbreviation.

Return type:

Dict[str, Dict[str, str]]

get_state_by_abbreviation(abbreviation)[source]

Get state information by abbreviation.

Parameters:

abbreviation (str)

Return type:

Dict[str, str] | None

get_state_by_name(name)[source]

Get state information by name (case-insensitive partial match).

Parameters:

name (str)

Return type:

Dict[str, str] | None

validate_state_fips(state_fips)[source]

Validate if a state FIPS code is valid.

Parameters:

state_fips (str)

Return type:

bool

get_state_name(state_fips)[source]

Get state name from FIPS code.

Parameters:

state_fips (str)

Return type:

str | None

get_state_abbreviation(state_fips)[source]

Get state abbreviation from FIPS code.

Parameters:

state_fips (str)

Return type:

str | None

class siege_utilities.geo.SpatialDataSource[source]

Bases: object

Base class for spatial data sources.

__init__(name, base_url, api_key=None)[source]

Initialize spatial data source.

Parameters:
  • name (str) – Name of the data source

  • base_url (str) – Base URL for the data source

  • api_key (str | None) – API key if required

download_data(**kwargs)[source]

Download data from the source.

Return type:

geopandas.GeoDataFrame | None

class siege_utilities.geo.GovernmentDataSource[source]

Bases: SpatialDataSource

Government data portal spatial data source.

__init__(portal_url, api_key=None)[source]

Initialize government data source.

Parameters:
  • portal_url (str) – Base URL for the government data portal

  • api_key (str | None) – API key if required

download_dataset(dataset_id, format_type='geojson')[source]

Download a dataset from the government data portal.

Parameters:
  • dataset_id (str) – Unique identifier for the dataset

  • format_type (str) – Preferred format (geojson, shapefile, kml)

Returns:

GeoDataFrame with spatial data.

Raises:

SpatialDataError – On any download or processing failure.

Return type:

geopandas.GeoDataFrame

class siege_utilities.geo.OpenStreetMapDataSource[source]

Bases: SpatialDataSource

OpenStreetMap data source using Overpass API.

__init__()[source]

Initialize OpenStreetMap data source.

download_osm_data(query, bbox=None)[source]

Download data from OpenStreetMap using Overpass QL.

Parameters:
  • query (str) – Overpass QL query string

  • bbox (List[float] | None) – Bounding box [min_lat, min_lon, max_lat, max_lon]

Returns:

GeoDataFrame with OSM data.

Raises:

SpatialDataError – On HTTP or processing failure.

Return type:

geopandas.GeoDataFrame

siege_utilities.geo.get_census_boundaries(year=2026, geographic_level='county', state_fips=None, state_identifier=None)[source]

Convenience function to get Census boundaries.

Deprecated since version 3.16.0: Use fetch_geographic_boundaries() (via CensusDataSource) for structured diagnostics via BoundaryFetchResult. get_census_boundaries will be removed in v3.17.0.

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (county, tract, etc.)

  • state_fips (str | None) – State FIPS code (e.g., ‘06’ for California)

  • state_identifier (str | None) – State identifier - accepts FIPS code, abbreviation, or name (e.g., ‘06’, ‘CA’, ‘California’ all work)

Return type:

geopandas.GeoDataFrame | None

Note

Provide either state_fips OR state_identifier, not both. If both are provided, state_identifier takes precedence.

Returns:

GeoDataFrame with Census boundaries (filtered by state if specified)

Parameters:
  • year (int)

  • geographic_level (str)

  • state_fips (str | None)

  • state_identifier (str | None)

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.download_data(year, geographic_level, state_fips=None, *, crs=None)[source]

Download Census data.

This is a convenience wrapper around get_geographic_boundaries().

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code (required for tract, block_group, etc.)

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

GeoDataFrame with boundaries in crs, or None if failed.

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.normalize_state_identifier(state_input)[source]

Normalize a state identifier to its 2-digit zero-padded FIPS code.

Parameters:

state_input (str) – Any of: 2-digit FIPS code ("48"), 2-letter abbreviation ("TX"), or full state name ("Texas"). Case-insensitive.

Returns:

Zero-padded 2-digit FIPS code (e.g., "06" for California).

Return type:

str

Raises:

ValueError – If state_input doesn’t match any known state, territory, or DC.

Examples

>>> normalize_state_identifier("TX")
'48'
>>> normalize_state_identifier("48")
'48'
>>> normalize_state_identifier("Texas")
'48'
>>> normalize_state_identifier("DC")
'11'
siege_utilities.geo.get_state_by_abbreviation(abbreviation)[source]

Get state info by abbreviation.

Parameters:

abbreviation (str)

Return type:

Dict[str, Any] | None

siege_utilities.geo.get_state_by_name(name)[source]

Get state info by name.

Parameters:

name (str)

Return type:

Dict[str, Any] | None

siege_utilities.geo.discover_boundary_types(year)[source]

Discover available boundary types for a year.

Returns the same {boundary_type: tiger_dir} mapping as CensusDataSource.discover_boundary_types(). The historical List[str] annotation never matched runtime behavior.

Parameters:

year (int)

Return type:

Dict[str, str]

siege_utilities.geo.get_census_data(api_key=None)[source]

Get Census data source instance.

Parameters:

api_key (str | None)

Return type:

CensusDataSource

siege_utilities.geo.download_osm_data(query, bbox=None, *, crs=None)[source]

Convenience function to download OSM data.

Parameters:
  • query (str) – OSM query string.

  • bbox (List[float] | None) – Optional bounding box [west, south, east, north].

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.get_available_years(force_refresh=False)[source]

Get available Census years.

Parameters:

force_refresh (bool)

Return type:

List[int]

siege_utilities.geo.get_year_directory_contents(year)[source]

Get directory contents for a specific year.

Parameters:

year (int)

Return type:

List[str]

siege_utilities.geo.construct_download_url(year, geographic_level, state_fips=None)[source]

Construct download URL for Census data.

Parameters:
  • year (int)

  • geographic_level (str)

  • state_fips (str | None)

Return type:

str

siege_utilities.geo.validate_download_url(url)[source]

Validate a download URL.

Parameters:

url (str)

Return type:

bool

siege_utilities.geo.get_optimal_year(geographic_level, preferred_year=None)[source]

Get optimal year for geographic level.

Parameters:
  • geographic_level (str) – Geographic level (county, tract, etc.)

  • preferred_year (int | None) – Preferred year (defaults to current year if None)

Returns:

Best available year for the requested boundary type

Return type:

int

siege_utilities.geo.get_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, state_identifier=None, *, crs=None)[source]

Get geographic boundaries (legacy, returns None on failure).

Deprecated since version 3.16.0: Use fetch_geographic_boundaries() for structured diagnostics. get_geographic_boundaries will be removed in v3.17.0.

Parameters:
Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.fetch_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, congress_number=None, *, crs=None)[source]

Get geographic boundaries with structured diagnostics.

Returns a BoundaryFetchResult instead of Optional[GeoDataFrame].

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code (required for tract, block_group, etc.)

  • congress_number (int | None) – Congress number for congressional districts

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

BoundaryFetchResult with .success, .geodataframe in crs, .error_stage, etc.

Return type:

BoundaryFetchResult

siege_utilities.geo.get_available_boundary_types(year)[source]

Get available boundary types for a year.

Returns {boundary_type: tiger_dir} – same shape as CensusDataSource.get_available_boundary_types(). The historical List[str] annotation never matched runtime behavior.

Parameters:

year (int)

Return type:

Dict[str, str]

siege_utilities.geo.refresh_discovery_cache()[source]

Refresh the discovery cache.

Return type:

None

siege_utilities.geo.get_available_state_fips()[source]

Get available state FIPS codes as a {fips_code: state_name} mapping.

Same shape as CensusDataSource.get_available_state_fips(). Historical List[str] annotation never matched runtime behavior.

Return type:

Dict[str, str]

siege_utilities.geo.get_state_abbreviations()[source]

Get a {fips_code: state_abbreviation} mapping.

Same shape as CensusDataSource.get_state_abbreviations(). Historical List[str] annotation never matched runtime behavior.

Return type:

Dict[str, str]

siege_utilities.geo.get_comprehensive_state_info()[source]

Get comprehensive state information.

Return type:

Dict[str, Dict[str, Any]]

siege_utilities.geo.validate_state_fips(fips)[source]

Validate state FIPS code.

Parameters:

fips (str)

Return type:

bool

siege_utilities.geo.get_state_name(fips)[source]

Get state name from FIPS code.

Parameters:

fips (str)

Return type:

str | None

siege_utilities.geo.get_state_abbreviation(fips)[source]

Get state abbreviation from FIPS code.

Parameters:

fips (str)

Return type:

str | None

siege_utilities.geo.download_dataset(year, geographic_level, state_fips=None, *, crs=None)[source]

Download Census dataset.

Parameters:
  • year (int) – Census year.

  • geographic_level (str) – Geographic level.

  • state_fips (str | None) – State FIPS code.

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.get_unified_fips_data()[source]

Get unified FIPS data with state names and abbreviations.

Return type:

Dict[str, Dict[str, Any]]

siege_utilities.geo.normalize_state_identifier_standalone(identifier)[source]

Normalize a state identifier to its 2-digit FIPS code.

Delegates to siege_utilities.config.census_registry.normalize_state_identifier(), the canonical implementation.

Parameters:

identifier (str) – Any of: 2-digit FIPS code, 2-letter abbreviation, or full state name.

Returns:

Zero-padded 2-digit FIPS code.

Return type:

str

Raises:

ValueError – If identifier doesn’t match any known state, territory, or DC.

siege_utilities.geo.normalize_state_input(raw_input)[source]

Normalize state input to handle formatting and trivial errors.

Parameters:

raw_input (str) – Raw state input (name, abbreviation, or FIPS)

Returns:

Normalized input (uppercase, trimmed, FIPS padded)

Return type:

str

Examples

normalize_state_input(” ca “) -> “CA” normalize_state_input(“CAlifornia”) -> “CALIFORNIA” normalize_state_input(“6”) -> “06”

siege_utilities.geo.normalize_state_name(name)[source]

Normalize state name input (e.g., “CAlifornia” -> “CALIFORNIA”).

Parameters:

name (str) – State name input

Returns:

Normalized state name in uppercase

Return type:

str

siege_utilities.geo.normalize_state_abbreviation(abbrev)[source]

Normalize state abbreviation input (e.g., “ ca “ -> “CA”).

Parameters:

abbrev (str) – State abbreviation input

Returns:

Normalized state abbreviation in uppercase

Return type:

str

siege_utilities.geo.normalize_fips_code(fips)[source]

Normalize FIPS code input (e.g., 6 -> “06”, “6” -> “06”).

Parameters:

fips (str | int) – FIPS code as string or integer

Returns:

Normalized FIPS code as 2-digit string

Return type:

str

class siege_utilities.geo.CensusDatasetMapper[source]

Bases: object

Maps relationships between Census datasets and provides intelligent data selection.

This class helps users understand: - Which datasets are available for different geography levels - How different survey types relate to each other - When to use different datasets based on analysis needs - Data quality and reliability considerations

__init__()[source]
get_dataset_info(dataset_name)[source]

Get information about a specific dataset.

Parameters:

dataset_name (str)

Return type:

CensusDataset | None

list_datasets_by_type(survey_type)[source]

List all datasets of a specific survey type.

Parameters:

survey_type (SurveyType)

Return type:

List[CensusDataset]

list_datasets_by_geography(geography_level)[source]

List all datasets available for a specific geography level.

Parameters:

geography_level (GeographyLevel)

Return type:

List[CensusDataset]

get_best_dataset_for_use_case(use_case, geography_level, time_period=None)[source]

Get the best dataset(s) for a specific use case.

Parameters:
  • use_case (str) – Description of what you’re trying to analyze

  • geography_level (GeographyLevel) – Required geography level

  • time_period (str | None) – Specific time period (optional)

Returns:

List of recommended datasets, ranked by suitability

Return type:

List[CensusDataset]

get_dataset_relationships(dataset_name)[source]

Get all relationships for a specific dataset.

Parameters:

dataset_name (str)

Return type:

List[DatasetRelationship]

compare_datasets(dataset1_name, dataset2_name)[source]

Compare two datasets side by side.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

get_data_selection_guide(analysis_type, geography_level, time_sensitivity='medium')[source]

Get a comprehensive guide for selecting Census data.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “business”, “housing”)

  • geography_level (GeographyLevel) – Required geography level

  • time_sensitivity (str) – How time-sensitive the analysis is (“low”, “medium”, “high”)

Returns:

Dictionary with data selection recommendations

Return type:

Dict[str, Any]

export_dataset_catalog(filepath)[source]

Export the complete dataset catalog to JSON.

Parameters:

filepath (str)

class siege_utilities.geo.SurveyType[source]

Bases: Enum

Enumeration of Census survey types.

DECENNIAL = 'decennial'
ACS_1YR = 'acs_1yr'
ACS_3YR = 'acs_3yr'
ACS_5YR = 'acs_5yr'
CENSUS_BUSINESS = 'census_business'
POPULATION_ESTIMATES = 'population_estimates'
HOUSING_ESTIMATES = 'housing_estimates'
class siege_utilities.geo.GeographyLevel[source]

Bases: Enum

Enumeration of Census geography levels (canonical names).

NATION = 'nation'
REGION = 'region'
DIVISION = 'division'
STATE = 'state'
COUNTY = 'county'
COUSUB = 'cousub'
PLACE = 'place'
CD = 'cd'
SLDU = 'sldu'
SLDL = 'sldl'
TRACT = 'tract'
BLOCK_GROUP = 'block_group'
BLOCK = 'block'
ZCTA = 'zcta'
CBSA = 'cbsa'
PUMA = 'puma'
VTD = 'vtd'
VTD20 = 'vtd20'
TABBLOCK20 = 'tabblock20'
TABBLOCK10 = 'tabblock10'
COUNTY_SUBDIVISION = 'cousub'
CONGRESSIONAL_DISTRICT = 'cd'
STATE_LEGISLATIVE_DISTRICT = 'sldu'
ZIP_CODE = 'zcta'
VOTING_DISTRICT = 'vtd'
class siege_utilities.geo.DataReliability[source]

Bases: Enum

Enumeration of data reliability levels.

HIGH = 'high'
MEDIUM = 'medium'
LOW = 'low'
ESTIMATED = 'estimated'
class siege_utilities.geo.CensusDataset[source]

Bases: object

Represents a Census dataset with metadata.

dataset_id: str
name: str
survey_type: SurveyType
geography_levels: List[GeographyLevel]
time_period: str
reliability: DataReliability
description: str
variables: List[str]
limitations: List[str]
best_for: List[str]
alternatives: List[str]
data_quality_notes: str
last_updated: str
next_update: str
api_endpoint: str | None = None
download_url: str | None = None
__init__(dataset_id, name, survey_type, geography_levels, time_period, reliability, description, variables, limitations, best_for, alternatives, data_quality_notes, last_updated, next_update, api_endpoint=None, download_url=None)
Parameters:
Return type:

None

class siege_utilities.geo.DatasetRelationship[source]

Bases: object

Represents a relationship between Census datasets.

primary_dataset: str
related_dataset: str
relationship_type: str
description: str
when_to_use_primary: str
overlap_period: str | None = None
compatibility_score: float = 1.0
__init__(primary_dataset, related_dataset, relationship_type, description, when_to_use_primary, when_to_use_related, overlap_period=None, compatibility_score=1.0)
Parameters:
  • primary_dataset (str)

  • related_dataset (str)

  • relationship_type (str)

  • description (str)

  • when_to_use_primary (str)

  • when_to_use_related (str)

  • overlap_period (str | None)

  • compatibility_score (float)

Return type:

None

siege_utilities.geo.get_census_dataset_mapper()[source]

Get a configured Census dataset mapper instance.

Return type:

CensusDatasetMapper

siege_utilities.geo.get_best_dataset_for_analysis(analysis_type, geography_level, time_sensitivity='medium')[source]

Convenience function to get the best dataset for analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_sensitivity (str) – Time sensitivity level

Returns:

Data selection guide

Return type:

Dict[str, Any]

siege_utilities.geo.compare_census_datasets(dataset1_name, dataset2_name)[source]

Convenience function to compare two Census datasets.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

siege_utilities.geo.get_dataset_info(dataset_name)[source]

Convenience function to get information about a specific dataset.

Parameters:

dataset_name (str)

Return type:

CensusDataset | None

siege_utilities.geo.list_datasets_by_type(survey_type)[source]

Convenience function to list all datasets of a specific survey type.

Parameters:

survey_type (str)

Return type:

List[CensusDataset]

siege_utilities.geo.list_datasets_by_geography(geography_level)[source]

Convenience function to list all datasets for a specific geography level.

Parameters:

geography_level (str)

Return type:

List[CensusDataset]

siege_utilities.geo.get_best_dataset_for_use_case(use_case, geography_level='county', time_sensitivity='medium')[source]

Convenience function to get the best dataset for a specific use case.

Parameters:
  • use_case (str)

  • geography_level (str)

  • time_sensitivity (str)

Return type:

Dict[str, Any]

siege_utilities.geo.get_dataset_relationships(dataset_name)[source]

Convenience function to get relationships for a dataset.

Parameters:

dataset_name (str)

Return type:

List[DatasetRelationship]

siege_utilities.geo.compare_datasets(dataset1_name, dataset2_name)[source]

Convenience function to compare two datasets.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

siege_utilities.geo.get_data_selection_guide(analysis_type, geography_level, time_sensitivity='medium')[source]

Convenience function to get data selection guide.

Parameters:
  • analysis_type (str)

  • geography_level (str)

  • time_sensitivity (str)

Return type:

Dict[str, Any]

siege_utilities.geo.export_dataset_catalog(filepath)[source]

Convenience function to export the dataset catalog.

Parameters:

filepath (str)

class siege_utilities.geo.CensusDataSelector[source]

Bases: object

Intelligent selector for Census data based on analysis requirements.

This class automatically determines the best Census datasets to use for different types of analysis, considering factors like: - Geography level requirements - Time sensitivity - Data reliability needs - Variable requirements - Analysis purpose

__init__()[source]
select_datasets_for_analysis(analysis_type, geography_level, time_period=None, variables=None, reliability_requirement=None)[source]

Select the best Census datasets for a specific analysis.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “housing”, “business”)

  • geography_level (str | GeographyLevel) – Required geography level

  • time_period (str | None) – Specific time period (optional)

  • variables (List[str] | None) – Required variables (optional)

  • reliability_requirement (str | None) – Reliability requirement (“low”, “medium”, “high”)

Returns:

Dictionary with dataset recommendations and rationale. Suitability scores use 0-5 scale: 0-2.0 (poor), 2.1-3.0 (moderate), 3.1-4.0 (good), 4.1-5.0 (excellent match)

Return type:

Dict[str, Any]

get_dataset_compatibility_matrix(analysis_types=None)[source]

Generate a compatibility matrix showing which datasets work best for different analysis types.

Parameters:

analysis_types (List[str] | None) – List of analysis types to include (default: all)

Returns:

DataFrame with compatibility scores

Return type:

pandas.DataFrame

suggest_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Suggest the best approach for conducting a specific analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str | GeographyLevel) – Required geography level

  • time_constraints (str | None) – Time constraints (“quick”, “standard”, “comprehensive”)

Returns:

Dictionary with analysis approach recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.get_census_data_selector()[source]

Get a configured Census data selector instance.

Return type:

CensusDataSelector

siege_utilities.geo.select_census_datasets(analysis_type, geography_level, time_period=None, variables=None)[source]

Convenience function to select Census datasets for analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_period (str | None) – Specific time period (optional)

  • variables (List[str] | None) – Required variables (optional)

Returns:

Dataset recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.select_datasets_for_analysis(analysis_type, geography_level, time_period=None, variables=None)[source]

Standalone function to select datasets for analysis.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “housing”, “business”)

  • geography_level (str) – Required geography level (e.g., “tract”, “county”, “state”)

  • time_period (str | None) – Time period preference (e.g., “latest”, “comprehensive”)

  • variables (List[str] | None) – List of required variables

Returns:

Dataset recommendations and rationale

Return type:

Dict[str, Any]

siege_utilities.geo.get_dataset_compatibility_matrix(analysis_types=None)[source]

Standalone function to get dataset compatibility matrix.

Parameters:

analysis_types (List[str] | None) – List of analysis types to include (optional)

Returns:

DataFrame with compatibility scores

Return type:

pandas.DataFrame

siege_utilities.geo.get_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Convenience function to get analysis approach recommendations.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_constraints (str | None) – Time constraints (optional)

Returns:

Analysis approach recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.suggest_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Standalone function to suggest analysis approach.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str | GeographyLevel) – Required geography level

  • time_constraints (str | None) – Time constraints (“quick”, “standard”, “comprehensive”)

Returns:

Dictionary with analysis approach recommendations

Return type:

Dict[str, Any]

class siege_utilities.geo.SpatialDataTransformer[source]

Bases: object

Transform spatial data between different formats and coordinate systems.

__init__()[source]

Initialize the spatial data transformer.

convert_format(gdf, output_format, **kwargs)[source]

Convert GeoDataFrame to different format.

Parameters:
  • gdf (None) – Input GeoDataFrame

  • output_format (str) – Desired output format

  • **kwargs – Additional format-specific parameters

Raises:
Return type:

None

class siege_utilities.geo.SWMapsArchive[source]

Bases: object

Context-manager handle to an opened SWMaps archive or raw SQLite file.

Use open_swmaps() to construct. Call close() (or use as a with block) to clean up any extracted-archive temp directory.

__init__(db_path, _temp_dir=None)[source]
Parameters:
  • db_path (str)

  • _temp_dir (str | None)

property db_path: str

Filesystem path to the readable SQLite database.

close()[source]

Remove the temp directory if this archive owned one.

Return type:

None

siege_utilities.geo.open_swmaps(path)[source]

Open a SWMaps archive (.swmz / .zip) or raw SQLite file.

Parameters:

path (str) – Path to a .swmz archive, a .zip archive containing a .sqlite member, or a raw .sqlite / .db file.

Returns:

An SWMapsArchive handle. Use it as a context manager so any extracted temp directory is cleaned up on exit.

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – If an archive contains no SQLite member, or if the file extension is not one of the recognized forms.

Return type:

SWMapsArchive

siege_utilities.geo.read_features(archive, mapper=None)[source]

Yield per-feature dicts from a SWMaps archive.

Each yielded dict has:

  • feature_id — SWMaps UUID.

  • feature_type — from feature_layers.name.

  • feature_group — from feature_layers.group_name.

  • layer_id — from feature_layers.uuid.

  • feature_name — from features.name.

  • geometry_wkt — POINT (single coordinate) or LINESTRING (two or more coordinates), built via shapely. Z dimension included when every coordinate row carries a non-null elv; dropped otherwise.

  • attributes{field_name: value} mapping pulled from attribute_values joined to attribute_fields. Keys can be renamed by passing mapper.

Parameters:
  • archive (SWMapsArchive) – An SWMapsArchive opened via open_swmaps().

  • mapper (dict[str, str] | None) – Optional {source_name: consumer_name} rename map. When None, attribute keys are the verbatim SWMaps field names. Unknown source keys are logged and skipped (kept verbatim).

Yields:

Dicts as described above. Empty-features (zero coordinate rows) are logged at WARNING and skipped.

Raises:

ImportError – If shapely is not installed.

Return type:

Iterator[dict]

The geometry rule matches the salvage source (DataUtils.py:749-768) but uses shapely.geometry.Point / shapely.geometry.LineString with .wkt rather than manual string concatenation.

siege_utilities.geo.find_vector_dataset_file_in_directory(directory, extensions=None)[source]

Find a vector dataset file in a directory.

Searches directory recursively for files matching common geospatial vector formats. Returns the first match found.

Parameters:
  • directory – Directory path to search (str or Path).

  • extensions (Sequence[str] | None) – Sequence of file extensions to match, including the leading dot (e.g. [".shp", ".geojson"]). Defaults to VALID_VECTOR_EXTENSIONS.

Returns:

Path to the first matching file, or None if no vector file is found.

Return type:

Path | None

siege_utilities.geo.concatenate_addresses(street=None, city=None, state_province_area=None, postal_code=None, country=None)[source]

Concatenate address components into a single string suitable for geocoding. Returns a properly formatted address string.

siege_utilities.geo.use_nominatim_geocoder(query_address, id=None, country_codes=None, max_retries=3, server_url=None)[source]

Geocode an address using Nominatim with proper rate limiting and error handling. Returns the result as a JSON string for Spark UDF compatibility.

Parameters:
  • query_address – The address to geocode

  • id – An identifier for tracking

  • country_codes – Optional country code filter (defaults to US)

  • max_retries – Number of retry attempts for transient errors

  • server_url – Optional custom Nominatim server URL (e.g., “http://nominatim.nominatim.svc.cluster.local” for self-hosted). Defaults to None (public OSM Nominatim with rate limiting). When set, rate limiting is skipped (self-hosted servers don’t require it).

Returns:

JSON string of the geocoding result, or None when Nominatim returned no match for the address (a legitimate non-error outcome).

Raises:
  • ValueError – If query_address is empty or None.

  • GeocodingError – If geocoding failed due to a network, service, or parse error after retries (i.e., the geocoder could not even attempt to match). Wraps the underlying geopy exception via __cause__.

class siege_utilities.geo.NominatimGeoClassifier[source]

Bases: object

A classifier for geocoding results using Nominatim. Provides methods to categorize and analyze geocoding results.

__init__()[source]
get_place_ranks_by_label(label)[source]

Reverse lookup on place_rank_dict: label → matching rank IDs.

Nominatim returns an integer place_rank per result (0=country … 10=building). This helper goes the other direction so callers can write filter(rank=cls.get_place_ranks_by_label("City")) without hard-coding the integer.

Returns [] (not None) when the label is unknown so the caller can in / iterate without a None-check.

get_importance_threshold_by_label(label)[source]

Reverse lookup on importance_dict: label → importance threshold.

Nominatim’s importance field is a float in [0, 1] that mixes Wikipedia hit-count, place type, and population. We bucket those floats into ordinal labels (Country / State / …) and this method returns the float threshold that corresponds to a label — useful when filtering a Nominatim result set programmatically.

Returns None when no label matches; callers should treat that as “don’t filter on importance.”

to_json()[source]

Serialize the classifier’s lookup tables to a JSON string.

Used to persist customised rank/importance overrides between runs or to ship a classifier configuration to a worker process. The result round-trips through from_json().

from_json(json_string)[source]

Replace the classifier’s lookup tables from a JSON string.

Parameters:

json_string – JSON produced by to_json().

Inverse of to_json() — overwrites place_rank_dict and importance_dict in place. Unknown top-level keys are ignored; missing keys leave the corresponding table empty rather than raising, so a partial payload doesn’t poison the classifier.

siege_utilities.geo.get_country_name(country_code)[source]

Get the full country name from a country code.

Parameters:

country_code – Two-letter country code (e.g., ‘us’, ‘gb’, ‘ca’)

Returns:

Full country name or the code if not found

Return type:

str

siege_utilities.geo.get_country_code(country_name)[source]

Get the country code from a country name.

Parameters:

country_name – Full country name (e.g., ‘United States’, ‘Canada’)

Returns:

Two-letter country code, or None if not found.

Return type:

str | None

siege_utilities.geo.list_countries()[source]

Get a list of all available countries with their codes.

Returns:

Dictionary mapping country codes to country names

Return type:

dict

siege_utilities.geo.get_coordinates(query_address, country_codes=None, max_retries=3, server_url=None)[source]

Get coordinates (latitude, longitude) for an address using Nominatim.

Parameters:
  • query_address – The address to geocode

  • country_codes – Optional country code filter (defaults to US)

  • max_retries – Maximum number of retry attempts

  • server_url – Optional custom Nominatim server URL (e.g., “http://nominatim.nominatim.svc.cluster.local” for self-hosted). Defaults to None (public OSM Nominatim).

Returns:

(latitude, longitude), or None when Nominatim returned no match for the address (a legitimate non-error outcome).

Return type:

tuple

Raises:
  • ValueError – If query_address is empty or None.

  • GeocodingError – If geocoding failed due to a network, service, or parse error — i.e., the geocoder could not even attempt to match. Wraps the underlying exception via __cause__.

siege_utilities.geo.geocode_with_nominatim_public(address, country_codes=None)[source]

Geocode a single address using the public Nominatim service.

Thin wrapper around get_coordinates() that forces the public endpoint (no custom server_url).

Parameters:
  • address – Address string to geocode.

  • country_codes – Optional country code filter (defaults to US).

Returns:

(lat, lon) tuple, or None if no match found.

Raises:
siege_utilities.geo.geocode_addresses_with_nominatim(addresses, country_codes=None, max_retries=3, server_url=None)[source]

Batch-geocode a list of addresses using Nominatim.

Calls get_coordinates() for each address sequentially with Nominatim’s built-in rate limiting (1 s for public, 50 ms for self-hosted).

Parameters:
  • addresses – Iterable of address strings.

  • country_codes – Optional country code filter (defaults to US).

  • max_retries – Retry attempts per address.

  • server_url – Optional custom Nominatim server URL.

Returns:

List of {"address": str, "lat": float, "lon": float} dicts. Addresses that returned no match have lat and lon set to None. Addresses that raised GeocodingError are logged and included with None coordinates.

exception siege_utilities.geo.GeocodingError[source]

Bases: RuntimeError

Raised when Nominatim geocoding fails for reasons other than ‘no match’.

Covers network timeouts (after retries exhausted), service errors, parse errors, and other unexpected failures from the geocoder. Distinct from a legitimate “no result found” outcome, which still returns None. Use __cause__ to inspect the underlying exception.

Mirrors siege_utilities.geo.providers.census_geocoder.CensusGeocodeError.

siege_utilities.geo.validate_geocode_data_pandas(df, lat_col, lon_col, crs=None)[source]

Filter rows with invalid geographic coordinates.

Pandas equivalent of spark_utils.validate_geocode_data().

Parameters:
  • df (pandas.DataFrame) – Input DataFrame.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • crs – Optional CRS (pyproj CRS, EPSG int, or string). When supplied, the valid coordinate bounds are derived from the CRS area of use. Defaults to WGS84 (-90..90, -180..180).

Returns:

DataFrame with only rows whose coordinates fall within the valid range.

Return type:

pandas.DataFrame

siege_utilities.geo.mark_valid_geocode_data_pandas(df, lat_col, lon_col, output_col='is_valid', crs=None)[source]

Add a boolean validity column without removing rows.

Pandas equivalent of spark_utils.mark_valid_geocode_data().

Parameters:
  • df (pandas.DataFrame) – Input DataFrame.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • output_col (str) – Name of the boolean column to add.

  • crs – Optional CRS (pyproj CRS, EPSG int, or string). When supplied, the valid coordinate bounds are derived from the CRS area of use. Defaults to WGS84 (-90..90, -180..180).

Returns:

Copy of df with output_col appended.

Return type:

pandas.DataFrame

exception siege_utilities.geo.IsochroneError[source]

Bases: Exception

Base exception for isochrone operations.

exception siege_utilities.geo.IsochroneNetworkError[source]

Bases: IsochroneError

Network failure (timeout, connection refused, DNS resolution).

exception siege_utilities.geo.IsochroneProviderError[source]

Bases: IsochroneError

Provider returned an HTTP error or an unparseable response.

class siege_utilities.geo.IsochroneRequest[source]

Bases: TypedDict

Structured request definition returned by build_isochrone_request().

provider: Literal['openrouteservice', 'valhalla']
method: Literal['POST', 'GET']
url: str
headers: Dict[str, str]
params: Dict[str, Any]
json: Dict[str, Any]
class siege_utilities.geo.IsochroneProvider[source]

Bases: ABC

Abstract base class for isochrone providers.

Subclasses must implement fetch(), validate_config(), and the provider_name property.

abstractmethod fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

abstractmethod validate_config()[source]

Check whether this provider is properly configured.

Returns:

True if the provider configuration is valid, False otherwise.

Return type:

bool

abstract property provider_name: str

Return the canonical name of this provider.

class siege_utilities.geo.OpenRouteServiceProvider[source]

Bases: IsochroneProvider

Concrete IsochroneProvider wrapping the OpenRouteService API.

Parameters:
  • api_key – ORS API key. Required for the hosted service; optional for self-hosted instances.

  • base_url – Root URL of the ORS server. Defaults to DEFAULT_ORS_BASE_URL.

  • timeout_seconds – HTTP timeout in seconds (default 30).

  • max_retries – Maximum retry attempts for transient failures (default 3).

__init__(api_key=None, base_url=None, timeout_seconds=30, max_retries=3)[source]
Parameters:
  • api_key (str | None)

  • base_url (str | None)

  • timeout_seconds (int)

  • max_retries (int)

Return type:

None

property provider_name: str

Return the canonical name of this provider.

validate_config()[source]

Check that the provider is configured.

For the hosted ORS service (default URL), an API key is required. For self-hosted instances, the base URL must be non-empty.

Return type:

bool

fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

class siege_utilities.geo.ValhallaProvider[source]

Bases: IsochroneProvider

Concrete IsochroneProvider wrapping a Valhalla server.

Parameters:
  • base_url – Root URL of the Valhalla server. Defaults to DEFAULT_VALHALLA_BASE_URL.

  • timeout_seconds – HTTP timeout in seconds (default 30).

  • max_retries – Maximum retry attempts for transient failures (default 3).

__init__(base_url=None, timeout_seconds=30, max_retries=3)[source]
Parameters:
  • base_url (str | None)

  • timeout_seconds (int)

  • max_retries (int)

Return type:

None

property provider_name: str

Return the canonical name of this provider.

validate_config()[source]

Check that the base URL is configured.

Return type:

bool

fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

siege_utilities.geo.build_isochrone_request(latitude, longitude, travel_time_minutes, *, provider='openrouteservice', base_url=None, profile='driving-car', api_key=None, extra_params=None)[source]

Build a provider-specific HTTP request definition for an isochrone call.

This supports open-source providers and self-hosted/custom endpoints.

Parameters:
  • latitude (float) – Center latitude (-90 to 90).

  • longitude (float) – Center longitude (-180 to 180).

  • travel_time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • provider (str) – "openrouteservice" (or "ors") or "valhalla".

  • base_url (str | None) – Custom server root for self-hosted providers.

  • profile (str) – Travel profile ("driving-car", "cycling-regular", "foot-walking", etc.).

  • api_key (str | None) – Optional API key (commonly used for hosted ORS).

  • extra_params (Mapping[str, Any] | None) – Provider-specific extra request fields merged into the JSON body (Valhalla) or query params (ORS).

Returns:

An IsochroneRequest dict with provider, method, url, headers, params, and json keys.

Raises:

ValueError – If inputs are out of range or provider is unknown.

Return type:

IsochroneRequest

siege_utilities.geo.get_isochrone(latitude, longitude, travel_time_minutes, *, provider='openrouteservice', base_url=None, profile='driving-car', api_key=None, timeout_seconds=30, extra_params=None, max_retries=3)[source]

Fetch an isochrone GeoJSON response.

Parameters:
  • latitude (float) – Center latitude.

  • longitude (float) – Center longitude.

  • travel_time_minutes (int) – Travel-time contour in minutes.

  • provider (str) – "openrouteservice" (default) or "valhalla".

  • base_url (str | None) – Custom server root for self-hosted providers.

  • profile (str) – Travel profile ("driving-car", "cycling-regular", "foot-walking", etc.).

  • api_key (str | None) – Optional API key (commonly used for hosted ORS).

  • timeout_seconds (int) – HTTP timeout in seconds (default 30).

  • extra_params (Mapping[str, Any] | None) – Provider-specific extra request fields.

  • max_retries (int) – Maximum number of attempts for transient failures (default 3). Set to 1 to disable retries.

Returns:

Provider response parsed as JSON (typically a GeoJSON FeatureCollection).

Raises:
Return type:

Dict[str, Any]

siege_utilities.geo.isochrone_to_geodataframe(isochrone_geojson, *, crs=None)[source]

Convert an isochrone GeoJSON object to a GeoDataFrame.

Requires geopandas. Install with pip install siege_utilities[geo].

GeoJSON is always ingested as EPSG:4326 (the GeoJSON spec mandates WGS 84). If crs differs from EPSG:4326 the result is reprojected to the requested CRS before returning.

Parameters:
  • isochrone_geojson (Mapping[str, Any]) – A GeoJSON dict (typically a FeatureCollection returned by get_isochrone()).

  • crs (str | None) – Target coordinate reference system. Defaults to get_default_crs() (initially "EPSG:4326"). Pass any string accepted by pyproj.

Returns:

A GeoDataFrame with one row per feature in the requested crs.

Raises:

ImportError – If geopandas is not installed.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.get_provider(name='ors', **kwargs)[source]

Factory function returning a configured IsochroneProvider.

Parameters:
  • name (str) – Provider name — "ors" / "openrouteservice" or "valhalla".

  • **kwargs (Any) – Passed through to the provider constructor (e.g. api_key, base_url, timeout_seconds).

Returns:

A configured provider instance.

Raises:

ValueError – If name is not a recognised provider.

Return type:

IsochroneProvider

exception siege_utilities.geo.CensusGeocodeError[source]

Bases: RuntimeError

Raised when the Census geocoder API call fails unexpectedly.

Distinct from “no match” results (which return a CensusGeocodeResult with matched=False). This exception indicates an API / network / parse failure where the geocoder could not even attempt to match. Use __cause__ to inspect the underlying exception.

class siege_utilities.geo.CensusVintage[source]

Bases: str, Enum

Census geocoder benchmark/vintage pairs.

Each member’s value encodes <benchmark>|<vintage> where both halves are the exact strings the Census Geocoder API expects. Use the .benchmark and .vintage properties to extract them.

The benchmark names follow the Public_AR_<label> pattern used by https://geocoding.geo.census.gov/geocoder/benchmarks. The vintage names follow the <label>_<benchmark-label> pattern returned by https://geocoding.geo.census.gov/geocoder/vintages?benchmark=….

CENSUS_2010 = 'Public_AR_Census2020|Census2010_Census2020'
CENSUS_2020 = 'Public_AR_Census2020|Census2020_Census2020'
CURRENT = 'Public_AR_Current|Current_Current'
ACS_2022 = 'Public_AR_Current|ACS2022_Current'
ACS_2023 = 'Public_AR_Current|ACS2023_Current'
property benchmark: str

The benchmark string accepted by the Census Geocoder API.

property vintage: str

The vintage string accepted by the Census Geocoder API.

__new__(value)
class siege_utilities.geo.CensusGeocodeResult[source]

Bases: object

Result from Census Bureau geocoding.

matched

Whether the address was matched.

Type:

bool

input_address

The original input address string.

Type:

str

matched_address

The standardized matched address (if matched).

Type:

str

lat

Latitude (if matched).

Type:

float | None

lon

Longitude (if matched).

Type:

float | None

state_fips

2-digit state FIPS code.

Type:

str

county_fips

3-digit county FIPS code.

Type:

str

tract

6-digit tract code.

Type:

str

block

4-digit block code.

Type:

str

match_type

“Exact”, “Non_Exact”, or “No_Match”.

Type:

str

side

Street side (“L” or “R”, from TIGER).

Type:

str

tiger_line_id

TIGER/Line feature ID.

Type:

str

matched: bool = False
input_address: str = ''
matched_address: str = ''
lat: float | None = None
lon: float | None = None
state_fips: str = ''
county_fips: str = ''
tract: str = ''
block: str = ''
match_type: str = 'No_Match'
side: str = ''
tiger_line_id: str = ''
input_id: str = ''
property state_geoid: str

2-digit state GEOID.

property county_geoid: str

5-digit county GEOID (state + county FIPS).

property tract_geoid: str

11-digit tract GEOID (state + county + tract).

property block_geoid: str

15-digit block GEOID (state + county + tract + block).

property block_group_geoid: str

12-digit block group GEOID (block GEOID truncated to 12 chars).

__init__(matched=False, input_address='', matched_address='', lat=None, lon=None, state_fips='', county_fips='', tract='', block='', match_type='No_Match', side='', tiger_line_id='', input_id='')
Parameters:
Return type:

None

siege_utilities.geo.select_vintage_for_cycle(year)[source]

Select the appropriate Census vintage for an FEC cycle year.

Parameters:

year (int) – The FEC election cycle year.

Returns:

CensusVintage matching the boundaries in effect for that cycle.

Return type:

CensusVintage

Examples

>>> select_vintage_for_cycle(2024)
<CensusVintage.CURRENT: 'Public_AR_Current|Current_Current'>
>>> select_vintage_for_cycle(2016)
<CensusVintage.CENSUS_2020: 'Public_AR_Census2020|Census2020_Census2020'>
>>> select_vintage_for_cycle(2008)
<CensusVintage.CENSUS_2010: 'Public_AR_Census2020|Census2010_Census2020'>
siege_utilities.geo.geocode_single(street, city, state, zipcode, vintage=CensusVintage.CURRENT)[source]

Geocode a single address via the Census Bureau API.

Parameters:
  • street (str) – Street address (e.g., “1600 Pennsylvania Ave NW”).

  • city (str) – City name.

  • state (str) – State abbreviation or name.

  • zipcode (str) – ZIP code.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

CensusGeocodeResult with lat/lon and FIPS codes if matched.

Return type:

CensusGeocodeResult

siege_utilities.geo.geocode_batch(addresses, vintage=CensusVintage.CURRENT)[source]

Geocode a batch of addresses via the Census Bureau batch API.

The Census batch API accepts up to 10,000 addresses per request. Each address dict should have keys: id, street, city, state, zipcode.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

List of CensusGeocodeResult, one per input address (order preserved).

Raises:

ValueError – If batch exceeds 10,000 addresses.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.geocode_batch_chunked(addresses, vintage=CensusVintage.CURRENT, chunk_size=10000)[source]

Geocode addresses in chunks of up to chunk_size (default 10,000).

Convenience wrapper around geocode_batch() for larger datasets.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

  • chunk_size (int) – Max addresses per API call (max 10,000).

Returns:

List of CensusGeocodeResult, one per input address.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.geocode_results_to_dataframe(results)[source]

Convert a list of CensusGeocodeResult to a pandas DataFrame.

Extracts all dataclass fields and computed GEOID properties into a flat DataFrame with standard column names. This eliminates the per-field list-comprehension boilerplate that every caller of geocode_batch() otherwise has to write.

Parameters:

results (list[CensusGeocodeResult]) – List of CensusGeocodeResult objects, typically returned by geocode_batch() or geocode_batch_chunked().

Returns:

input_id, input_address, matched, match_type, matched_address, latitude, longitude, state_geoid, county_geoid, tract_geoid, block_geoid, block_group_geoid, state_fips, county_fips, tract, block, side, tiger_line_id. For unmatched rows, latitude/longitude are None and GEOID columns are empty strings.

Return type:

DataFrame with columns

Raises:

TypeError – If results is not a list.

Example:

results = geocode_batch(addresses)
df = geocode_results_to_dataframe(results)
# df now has tract_geoid, block_geoid, lat/lon, etc.
siege_utilities.geo.is_valid_state_fips(value)[source]

Validate a 2-digit state FIPS code against known codes.

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.is_valid_county_fips(value)[source]

Validate a 5-digit county FIPS code (2-digit state + 3-digit county).

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.is_valid_tract_geoid(value)[source]

Validate an 11-digit tract GEOID (2-digit state + 3-digit county + 6-digit tract).

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.is_valid_block_group_geoid(value)[source]

Validate a 12-digit block group GEOID (state + county + tract + block group).

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.resolve_geographic_level(level)[source]

Resolve any geographic level name variant to its canonical form.

Accepts long forms (congressional_district), short forms (cd), Census abbreviations (CD), and common aliases (zip_code → zcta).

Parameters:

level (str)

Return type:

str

class siege_utilities.geo.LocaleCode[source]

Bases: object

An NCES 12-code locale classification result.

code

Integer locale code (11-43).

Type:

int

category

Major type — city, suburban, town, or rural.

Type:

str

subcategory

Full subtype — city_large, rural_remote, etc.

Type:

str

label

Human-readable label — City-Large, Rural-Remote, etc.

Type:

str

code: int
category: str
subcategory: str
label: str
classmethod from_code(code)[source]

Construct a LocaleCode from an integer code (11-43).

Raises:

ValueError – If the code is not a valid NCES locale code.

Parameters:

code (int)

Return type:

LocaleCode

__init__(code, category, subcategory, label)
Parameters:
Return type:

None

class siege_utilities.geo.LocaleType[source]

Bases: IntEnum

NCES major locale types.

CITY = 1
SUBURB = 2
TOWN = 3
RURAL = 4
__new__(value)
class siege_utilities.geo.NCESLocaleClassifier[source]

Bases: object

Classify geographic points and polygons into NCES locale codes.

Uses Census TIGER spatial data (Urbanized Areas, Urban Clusters, Places) and population thresholds to implement the NCES urban-centric locale classification algorithm (12-code system).

The classifier can be initialized with pre-loaded GeoDataFrames (for testing or custom data) or constructed from Census downloads via the from_census_year and from_nces_boundaries factory methods.

Parameters:
  • urbanized_areas – GeoDataFrame of UA polygons (pop >= 50,000). Must contain a geometry column and a population field.

  • urban_clusters – GeoDataFrame of UC polygons (pop 2,500-49,999). Must contain a geometry column. May be None for 2020+ Census data where UCs were eliminated.

  • principal_cities – GeoDataFrame of Place polygons that are designated principal cities of CBSAs.

  • place_populations – Dict mapping place identifiers to population counts. Used to classify City subtypes (Large/Midsize/Small).

  • ua_populations – Optional dict mapping UA identifiers to populations. Used to classify Suburb subtypes. If None, reads from the urbanized_areas GeoDataFrame attributes.

  • projection_crs – EPSG code for distance computations. Defaults to settings.PROJECTION_CRS (2163, US National Atlas Equal Area). Override for Alaska (3338) or Hawaii (26963).

__init__(urbanized_areas, urban_clusters, principal_cities, place_populations, ua_populations=None, projection_crs=None)[source]
Parameters:
  • urbanized_areas (Any)

  • urban_clusters (Any | None)

  • principal_cities (Any)

  • place_populations (Dict[str, int])

  • ua_populations (Dict[str, int] | None)

  • projection_crs (int | None)

property locale_index: LocaleIndex | None
classify_geoid(geoid)[source]

Fast-path locale lookup by Census GEOID.

Returns the precomputed locale code if the GEOID is in the index, or None if no index is set or the GEOID is not found. Use classify_geoid_or_point() for automatic fallback to spatial classification.

Parameters:

geoid (str)

Return type:

LocaleCode | None

classify_geoid_or_point(geoid, lon, lat)[source]

Classify by GEOID if available, falling back to spatial.

Parameters:
Return type:

LocaleCode

classify_point(lon, lat)[source]

Classify a single geographic point.

Implements the NCES decision tree: 1. Spatial join against Urbanized Areas 2. If in UA: check principal city → City or Suburb 3. If not in UA: check Urban Cluster → Town (by distance to UA) 4. If not in UA/UC → Rural (by distance to UA and UC)

Parameters:
  • lon (float) – Longitude in the input CRS (default NAD83/WGS84).

  • lat (float) – Latitude in the input CRS (default NAD83/WGS84).

Returns:

A LocaleCode describing the point’s NCES classification.

Return type:

LocaleCode

classify_points(gdf, geometry_col='geometry')[source]

Bulk classify a GeoDataFrame of points.

Adds columns: locale_code (int), locale_category (str), locale_subcategory (str), locale_label (str).

Parameters:
  • gdf (Any) – GeoDataFrame with point geometries.

  • geometry_col (str) – Name of the geometry column.

Returns:

The input GeoDataFrame with locale columns added.

Return type:

Any

classify_polygon(polygon, method='area_weighted')[source]

Classify a polygon by locale distribution.

Parameters:
  • polygon (Any) – A Shapely polygon or multipolygon geometry.

  • method (str) – "area_weighted", "majority", or "distribution".

Returns:

{"locale_code": 21, "locale_label": "..."} If method="distribution" or "area_weighted": dict of {code: fraction} pairs summing to ~1.0.

Return type:

If method="majority"

Raises:

ValueError – If method is not one of the valid options.

classify_polygons(gdf, method='majority', geometry_col='geometry')[source]

Bulk classify a GeoDataFrame of polygons.

For method="majority", adds columns: locale_code (int), locale_label (str).

For method="distribution" or "area_weighted", adds a locale_distribution column containing per-row dicts of {code: fraction} pairs.

Parameters:
  • gdf (Any) – GeoDataFrame with polygon geometries.

  • method (str) – "majority", "distribution", or "area_weighted".

  • geometry_col (str) – Name of the geometry column.

Returns:

The input GeoDataFrame with locale columns added.

Raises:

ValueError – If method is not one of the valid options.

Return type:

Any

classmethod from_census_year(year=2020, cache_dir=None, projection_crs=None)[source]

Construct a classifier from Census TIGER downloads.

Downloads UA/UC (uac), Place, and CBSA shapefiles for the given year, identifies principal cities from population thresholds, and loads population estimates from shapefile attributes.

For 2020+ Census data where Urban Clusters were eliminated, this method derives a UC-equivalent set by filtering urban areas with population < 50,000 (the original UC threshold). This ensures Town codes (31-33) remain reachable.

Principal cities are identified by population threshold (>= 25,000), not from the OMB CBSA delineation file. This is a best-effort approximation. For precise NCES-matching classification, use from_nces_boundaries() when available.

Parameters:
  • year (int) – Census year for boundary data (2010 or 2020 recommended).

  • cache_dir (str | None) – Local directory for caching downloaded shapefiles.

  • projection_crs (int | None) – EPSG code for distance computation.

Returns:

An initialized NCESLocaleClassifier.

Raises:

ImportError – If geopandas is not available.

Return type:

NCESLocaleClassifier

classmethod from_nces_boundaries(year=2023, cache_dir=None, projection_crs=None)[source]

Construct a classifier from NCES pre-computed locale boundary shapefiles.

NCES publishes locale territory polygons that are already classified with locale codes (11-43). This is faster than computing from Census inputs but limited to years NCES has published.

The downloaded boundaries are split into: - Urbanized Area proxies: City (11-13) + Suburb (21-23) territories - Urban Cluster proxies: Town (31-33) territories - Principal city proxies: City (11-13) territories only

The classifier then works identically to the Census-based one.

Parameters:
  • year (int) – NCES publication year.

  • cache_dir (str | None) – Local directory for caching downloaded shapefiles.

  • projection_crs (int | None) – EPSG code for distance computation.

Returns:

An initialized NCESLocaleClassifier.

Return type:

NCESLocaleClassifier

static locale_label(code)[source]

Convert an integer locale code to a human-readable label.

>>> NCESLocaleClassifier.locale_label(11)
'City-Large'
>>> NCESLocaleClassifier.locale_label(43)
'Rural-Remote'
Parameters:

code (int)

Return type:

str

siege_utilities.geo.locale_from_code(code)[source]

Fast lookup of a pre-built LocaleCode by integer code.

Returns the pre-built constant when possible, avoiding dataclass construction.

Parameters:

code (int)

Return type:

LocaleCode

class siege_utilities.geo.NCESDownloader[source]

Bases: object

Download NCES locale boundaries, school locations, and district data.

Downloads from the NCES EDGE (Education Demographic and Geographic Estimates) program and returns data as GeoDataFrames.

Parameters:
  • cache_dir – Directory for caching downloaded files. Defaults to a temporary directory.

  • timeout – HTTP request timeout in seconds.

Example:

downloader = NCESDownloader(cache_dir="./nces_cache")
boundaries = downloader.download_locale_boundaries(2023)
# GeoDataFrame with 12 locale territory polygons
__init__(cache_dir=None, timeout=120)[source]
Parameters:
  • cache_dir (str | None)

  • timeout (int)

download_locale_boundaries(year=2023, *, crs=None)[source]

Download NCES locale boundary polygons.

Returns a GeoDataFrame with 12 rows — one polygon per locale territory (codes 11-43). Each polygon represents the geographic extent of that locale classification.

Parameters:
Returns:

locale_code, locale_category, locale_subcategory, name, geometry.

Return type:

GeoDataFrame with columns

download_school_locations(year=2023, state_abbr=None, *, crs=None)[source]

Download geocoded school locations.

Returns a GeoDataFrame of school point locations with NCES locale codes.

Parameters:
  • year (int) – NCES publication year.

  • state_abbr (str | None) – Optional 2-letter state abbreviation to filter.

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

ncessch, school_name, lea_id, state_abbr, locale_code, locale_category, locale_subcategory, geometry.

Return type:

GeoDataFrame with columns

download_district_data(year=2023)[source]

Download district administrative data with locale codes.

Returns a DataFrame (not GeoDataFrame) of school district administrative records keyed by LEA ID.

Parameters:

year (int) – NCES publication year.

Returns:

lea_id, lea_name, state_abbr, locale_code, locale_category, locale_subcategory, survey_year.

Return type:

DataFrame with columns

exception siege_utilities.geo.NCESDownloadError[source]

Bases: Exception

Raised when an NCES download fails.

class siege_utilities.geo.Gazetteer[source]

Bases: Protocol

Name → geometry resolver.

The protocol is intentionally narrow — four methods. Backends layer LRU caches, ISO-code translation, and other plumbing internally; callers just see this surface.

property provider_name: str

Human-readable backend identifier ("wkls", etc.).

lookup(name, *, country_hint=None, admin_hint=None)[source]

Best match for name.

Parameters:
  • name (str) – Place name in human form ("Birmingham, AL").

  • country_hint (str | None) – Optional ISO-3166-1 alpha-2 ("US") or alpha-3 ("USA") hint to disambiguate places that exist in multiple countries.

  • admin_hint (str | None) – Optional region/state hint ("Alabama", "AL", "01").

Raises:
Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]

Top-N candidate matches.

Used to surface ambiguity to a caller (interactive UI, or programmatic disambiguation when the caller has external knowledge to pick a candidate).

Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

is_available()[source]

Return True if the backend’s dependencies are installed and the backend can answer queries.

Should NOT make a network round-trip — this is a fast probe used by the factory to pick a default backend.

Return type:

bool

__init__(*args, **kwargs)
class siege_utilities.geo.GazetteerResult[source]

Bases: object

A resolved place: name, hierarchy, geometry, centroid.

name

Display name of the matched place.

Type:

str

canonical_path

Hierarchical breadcrumb the source resolves the place through, e.g. ("US", "AL", "Birmingham"). Useful for downstream filtering and for re-querying the same backend without going through fuzzy search again.

Type:

tuple[str, …]

geometry

shapely shape (Polygon / MultiPolygon for admin areas; may be a Point for cities-as-points).

Type:

Any

centroid

shapely Point. Pre-computed because half the consumer code needs it and recomputing on every call is wasteful.

Type:

Any

country

Optional ISO-3166-1 alpha-2 country code.

Type:

str | None

admin_levels

Optional mapping of admin level name → value (e.g. {"region": "Alabama", "county": "Jefferson"}). Backend-specific; not assumed by the resolver layer.

Type:

Mapping[str, str]

source

Backend name ("wkls", "nominatim", etc.) — for audit / log lineage.

Type:

str

raw

Backend-native object for callers that need full fidelity. Excluded from repr and hash so the dataclass stays sane to print and stable as a cache key.

Type:

Any

name: str
canonical_path: tuple[str, ...]
geometry: Any
centroid: Any
country: str | None = None
admin_levels: Mapping[str, str]
source: str = ''
raw: Any = None
__init__(name, canonical_path, geometry, centroid, country=None, admin_levels=<factory>, source='', raw=None)
Parameters:
Return type:

None

class siege_utilities.geo.GazetteerCandidate[source]

Bases: object

A search hit — a place that might be the answer.

Search returns multiple of these; lookup picks one. Keep the structure small and JSON-safe so it round-trips through APIs and logs cleanly.

name: str
canonical_path: tuple[str, ...]
country: str | None = None
admin_levels: Mapping[str, str]
score: float | None = None
source: str = ''
__init__(name, canonical_path, country=None, admin_levels=<factory>, score=None, source='')
Parameters:
Return type:

None

exception siege_utilities.geo.GazetteerError[source]

Bases: RuntimeError

Base class for all gazetteer failures.

exception siege_utilities.geo.GazetteerNotFoundError[source]

Bases: GazetteerError

No place matched the lookup criteria.

exception siege_utilities.geo.GazetteerAmbiguousError[source]

Bases: GazetteerError

The query is ambiguous and the caller didn’t pass enough hints.

Carries the candidates so the caller can surface them to a user or pick programmatically.

__init__(message, candidates)[source]
Parameters:
exception siege_utilities.geo.GazetteerBackendError[source]

Bases: GazetteerError

The backend raised an error we couldn’t translate.

Network failure, malformed upstream response, missing optional dependency at lookup time. Always chains the original via __cause__.

siege_utilities.geo.resolve_gazetteer(*, prefer=None, cache_size=1024)[source]

Return a configured Gazetteer backend.

Selection order:

  1. If prefer is given, use that backend (raises if unavailable).

  2. Otherwise, try WKLS (global coverage, no API key).

  3. Then Nominatim (OSM-backed, public service).

  4. Raise if nothing works.

Parameters:
  • prefer (str | None) – "wkls" / "nominatim" / "census" / "wikidata" to force a specific backend.

  • cache_size (int) – LRU cache size for backends that support it.

Raises:
Return type:

Gazetteer

exception siege_utilities.geo.EtterError[source]

Bases: RuntimeError

Base class for connector-side Etter failures.

exception siege_utilities.geo.EtterParseError[source]

Bases: EtterError

Etter failed to parse the query into a structured filter.

exception siege_utilities.geo.EtterLowConfidenceError[source]

Bases: EtterError

Parser succeeded but confidence is below the threshold.

class siege_utilities.geo.EtterFilter[source]

Bases: object

A normalized parsed query ready for downstream consumption.

Wraps the upstream GeoQuery Pydantic model into a dataclass with the fields siege_utilities consumers actually need. The original upstream object is preserved on raw for callers that need full fidelity.

original_query

The natural-language input verbatim.

Type:

str

spatial_relation

One of the relations Etter recognises — "in", "near", "north_of", "south_of", "east_of", "west_of", etc. None if the query doesn’t carry a relation (e.g. a bare place name).

Type:

str | None

reference_location

The place the relation hangs off — "Lausanne", "Lake Geneva", "Birmingham, AL".

Type:

str | None

buffer_distance_m

Buffer distance in meters if the query had one ("5 km" → 5000). None otherwise.

Type:

float | None

confidence

Overall confidence score in [0, 1].

Type:

float

raw

The upstream etter.GeoQuery object, in case the consumer needs the full breakdown.

Type:

Any

original_query: str
spatial_relation: str | None
reference_location: str | None
buffer_distance_m: float | None = None
confidence: float = 1.0
raw: Any = None
classmethod from_geoquery(original_query, geo_query)[source]

Translate an upstream GeoQuery to an EtterFilter.

Tolerant of upstream field-name drift — uses getattr with defaults rather than positional unpacking.

Parameters:
  • original_query (str)

  • geo_query (Any)

Return type:

EtterFilter

__init__(original_query, spatial_relation, reference_location, buffer_distance_m=None, confidence=1.0, raw=None)
Parameters:
  • original_query (str)

  • spatial_relation (str | None)

  • reference_location (str | None)

  • buffer_distance_m (float | None)

  • confidence (float)

  • raw (Any)

Return type:

None

class siege_utilities.geo.EtterParser[source]

Bases: object

Wrap etter.GeoFilterParser with a sensible default LLM.

Usage:

from siege_utilities.geo.providers.etter_filter import EtterParser

# Defaults: gpt-4o, temperature=0, OPENAI_API_KEY from env.
parser = EtterParser()
result = parser.parse("5 km north of Lausanne")
# result.spatial_relation == "north_of"
# result.reference_location == "Lausanne"
# result.buffer_distance_m == 5000.0

Pass llm= to use a pre-built chat model (e.g. for tests or for pinning to a specific Anthropic / Azure deployment). All other constructor kwargs are forwarded to the upstream parser.

__init__(*, llm=None, confidence_threshold=0.6, strict_mode=False, **upstream_kwargs)[source]
Parameters:
  • llm (Any)

  • confidence_threshold (float)

  • strict_mode (bool)

  • upstream_kwargs (Any)

Return type:

None

parse(query)[source]

Parse query into an EtterFilter.

Raises:
  • EtterParseError – Upstream parser raised an unexpected error. The original exception is chained via __cause__.

  • EtterLowConfidenceError – Parser succeeded but confidence is below the threshold and strict_mode=True. (When strict_mode is False, this case logs and returns normally; check EtterFilter.confidence if the caller needs to gate.) Also raised if a future upstream exposes its own LowConfidenceError and our local check wasn’t reached — we re-raise it as our type so consumers don’t have to know about upstream class names.

Parameters:

query (str)

Return type:

EtterFilter

siege_utilities.geo.default_llm(*, model='gpt-4o', temperature=0.0, api_key=None)[source]

Return a langchain chat model suitable for EtterParser.

Picks the right adapter based on the model name. api_key falls back to the standard environment variable for the chosen provider (OPENAI_API_KEY / ANTHROPIC_API_KEY). Lazy-imports langchain so the rest of this module is importable without it.

Parameters:
  • model (str) – Chat model name (e.g. "gpt-4o", "claude-3-5-sonnet-latest").

  • temperature (float) – Sampling temperature; default 0.0 for reproducibility — query parsing is not creative writing.

  • api_key (str | None) – Optional explicit API key; otherwise read from env.

Return type:

Any

class siege_utilities.geo.RelationSemantics[source]

Bases: str, Enum

How to interpret directional/proximity relations.

See module docstring for the full rationale.

BOUNDED = 'bounded'
HALFPLANE = 'halfplane'
CONTAINS_CENTROID = 'contains_centroid'
__new__(value)
class siege_utilities.geo.EtterGeometryResult[source]

Bases: object

The resolved geometry plus diagnostic context.

geometry

The shapely geometry (or PointPredicate for CONTAINS_CENTROID mode).

Type:

Any

relation

The Etter spatial_relation that was applied, or None if the input had no relation (just a bare reference location → the reference geometry itself).

Type:

str | None

reference

The GazetteerResult that anchored the relation. None only when the filter had no reference_location either, which is a degenerate input.

Type:

siege_utilities.geo.gazetteers.base.GazetteerResult | None

buffer_km

Buffer distance actually used (filter wins over default; None for relations that don’t buffer).

Type:

float | None

semantics

Which RelationSemantics mode produced this result.

Type:

siege_utilities.geo.providers.etter_to_geometry.RelationSemantics

notes

Free-text diagnostic notes (e.g., “halfplane truncated to ±90° lat to remain valid GeoJSON”).

Type:

tuple[str, …]

geometry: Any
relation: str | None
reference: GazetteerResult | None
buffer_km: float | None
semantics: RelationSemantics
notes: tuple[str, ...] = ()
__init__(geometry, relation, reference, buffer_km, semantics, notes=())
Parameters:
Return type:

None

exception siege_utilities.geo.EtterToGeometryError[source]

Bases: RuntimeError

Raised when an EtterFilter cannot be turned into a geometry.

Carries the parsed filter and the failure reason. The most common causes — the reference location not being found in the gazetteer, or a relation Etter parsed that we don’t know how to translate — each have their own subclass so callers can branch.

exception siege_utilities.geo.EtterReferenceNotFoundError[source]

Bases: EtterToGeometryError

The reference location couldn’t be resolved by the gazetteer.

exception siege_utilities.geo.EtterUnknownRelationError[source]

Bases: EtterToGeometryError

Etter returned a relation we don’t have a translation for.

siege_utilities.geo.etter_to_geometry(filter_, *, gazetteer, semantics=RelationSemantics.BOUNDED, default_buffer_km=25.0, country_hint=None, admin_hint=None)[source]

Resolve an EtterFilter to a geometry.

Parameters:
  • filter – The parsed query.

  • gazetteer (Gazetteer) – The gazetteer that resolves the reference location. Pass the result of siege_utilities.geo.gazetteers.resolve_gazetteer().

  • semantics (RelationSemantics) – Which mode of relation interpretation to use. The default RelationSemantics.BOUNDED is safe for most indexed-lookup workloads.

  • default_buffer_km (float) – Buffer to apply for “near”-style relations and as the half-width of bounded directional buffers when the filter doesn’t carry an explicit distance.

  • admin_hint (str | None) – Forwarded to gazetteer.lookup to disambiguate the reference location. Etter often emits just the place name; the consumer may know the country from a sibling field.

  • filter_ (EtterFilter)

  • country_hint (str | None)

  • admin_hint

Returns:

EtterGeometryResult with the geometry (or a PointPredicate) and diagnostic context.

Raises:
Return type:

EtterGeometryResult

class siege_utilities.geo.SpatialLoaderPlan[source]

Bases: object

Chosen loader with deterministic fallback ordering.

primary_loader: str
loader_order: List[str]
reason: str
__init__(primary_loader, loader_order, reason)
Parameters:
Return type:

None

siege_utilities.geo.select_spatial_loader(ogr2ogr_available, sedona_available, native_spatial_available=False)[source]

Select a spatial loader strategy for Databricks-style environments.

Loader order: 1. databricks_native 2. ogr2ogr 3. sedona 4. python

Parameters:
  • ogr2ogr_available (bool)

  • sedona_available (bool)

  • native_spatial_available (bool)

Return type:

SpatialLoaderPlan

siege_utilities.geo.build_census_table_name(year, geography_level, prefix='census')[source]

Build a normalized census table name like census_2024_block_group.

Parameters:
  • year (int)

  • geography_level (str)

  • prefix (str)

Return type:

str

siege_utilities.geo.build_census_ingest_targets(year, geography_levels, prefix='census')[source]

Build de-duplicated target table names for ingest planning.

Parameters:
Return type:

List[str]

class siege_utilities.geo.SpatialRuntimePlan[source]

Bases: object

Execution plan for spatial operations with deterministic fallback order.

runtime: str
native_spatial_available: bool
sedona_available: bool
loader_order: List[str]
reason: str
__init__(runtime, native_spatial_available, sedona_available, loader_order, reason)
Parameters:
  • runtime (str)

  • native_spatial_available (bool)

  • sedona_available (bool)

  • loader_order (List[str])

  • reason (str)

Return type:

None

siege_utilities.geo.resolve_spatial_runtime_plan(*, databricks_runtime_version=None, native_spatial_available=None, sedona_available=None)[source]

Resolve spatial runtime execution plan.

Priority order: 1. databricks_native 2. sedona 3. python

Parameters:
  • databricks_runtime_version (str | None)

  • native_spatial_available (bool | None)

  • sedona_available (bool | None)

Return type:

SpatialRuntimePlan

class siege_utilities.geo.GeometryPayload[source]

Bases: object

Spark-safe container for a single geometry with optional multi-format encoding.

At least one of geometry_wkt, geometry_wkb, or geometry_geojson should be populated for the payload to be useful.

geometry_wkt: str | None = None
geometry_wkb: bytes | None = None
geometry_geojson: Dict[str, Any] | None = None
crs: str = 'EPSG:4326'
geometry_type: str | None = None
__init__(geometry_wkt=None, geometry_wkb=None, geometry_geojson=None, crs='EPSG:4326', geometry_type=None)
Parameters:
  • geometry_wkt (str | None)

  • geometry_wkb (bytes | None)

  • geometry_geojson (Dict[str, Any] | None)

  • crs (str)

  • geometry_type (str | None)

Return type:

None

siege_utilities.geo.encode_geometry(geom, fmt='wkt', crs='EPSG:4326')[source]

Encode a shapely geometry into a GeometryPayload.

Parameters:
  • geom (Any) – A shapely.geometry.base.BaseGeometry instance.

  • fmt (str) – Primary serialisation format – "wkt", "wkb", or "geojson". All three representations are always populated; fmt validates that the caller’s intended primary format is supported.

  • crs (str) – Coordinate reference system identifier stored on the payload.

Return type:

GeometryPayload

Raises:

ValueError – If fmt is not one of "wkt", "wkb", "geojson".

siege_utilities.geo.decode_geometry(payload)[source]

Decode a GeometryPayload back to a shapely geometry.

Tries WKT first, then WKB, then GeoJSON.

Return type:

shapely.geometry.base.BaseGeometry

Parameters:

payload (GeometryPayload)

siege_utilities.geo.payload_to_spark_row(payload)[source]

Convert a GeometryPayload to a Spark-safe dict.

Binary geometry_wkb is base64-encoded to a string; geometry_geojson is JSON-serialised. All resulting values are plain strings (or None).

Parameters:

payload (GeometryPayload)

Return type:

Dict[str, Any]

siege_utilities.geo.spark_row_to_payload(row)[source]

Reconstruct a GeometryPayload from a Spark-row dict.

Reverses the encoding performed by payload_to_spark_row().

Parameters:

row (Dict[str, Any])

Return type:

GeometryPayload

siege_utilities.geo.s2_index_points(df, lat_col, lon_col, level=12, *, as_token=True)[source]

Compute the S2 cell ID for each point.

Parameters:
  • df (pd.DataFrame) – DataFrame with latitude / longitude columns.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • level (int) – S2 level (0–30). Default 12 (~5.8 km² per cell).

  • as_token (bool) – If True (default), return the cell as a 16-char hex token string (compact, human-readable, JSON-safe). If False, return the int64 cell ID (sortable, ideal for database keys). Both round-trip via the s2_cell_id_to_token() / s2_token_to_cell_id() helpers.

Returns:

"s2_index".

Return type:

pd.Series indexed like the input. Name

Raises:
  • ImportError – If s2sphere is not installed.

  • ValueError – If level is out of range or columns are missing.

siege_utilities.geo.s2_index_polygon(geometry, level=12, *, refine=True)[source]

Return the set of S2 cells at level covering geometry.

Single-level coverage — symmetric with h3_index_polygon(). For variable-resolution coverage with a cell-count budget, see s2_region_cover().

Because the pure-Python s2sphere lacks polygon support, this function covers the polygon’s bounding box and (by default) post- filters the result to cells whose centers fall inside the actual geometry, using shapely. Set refine=False to skip the filter when the input is a near-rectangle (e.g. a county) and you want the bbox cover unchanged.

Parameters:
  • geometry – A shapely Polygon / MultiPolygon, or a GeoJSON-like dict.

  • level (int) – S2 level (0–30). All returned cells are at this level.

  • refine (bool) – Filter cells by checking whether the cell center lies inside the polygon (default True).

Returns:

set of int64 cell IDs.

Raises:
  • ImportError – If s2sphere (or shapely, when refine=True) is missing.

  • ValueError – If level is out of range.

  • TypeError – If geometry can’t be converted to a shapely shape.

Return type:

set

siege_utilities.geo.s2_region_cover(geometry, *, min_level=0, max_level=30, max_cells=100)[source]

Cover geometry’s bounding box with at most max_cells S2 cells.

The S2-specific operation: returns a mixed-resolution set of cells that together cover the bounding box, preferring coarse cells in the interior and fine cells along boundaries. The output is what you want for a database-side spatial-index lookup.

Use s2_cells_to_ranges() to turn the result into (min, max) int64 ranges suitable for a SQL BETWEEN clause.

Parameters:
  • geometry – shapely shape or GeoJSON dict.

  • min_level (int) – Minimum S2 level the coverer may use.

  • max_level (int) – Maximum S2 level the coverer may use.

  • max_cells (int) – Cell-count budget. The coverer returns at most this many cells (often fewer).

Returns:

A list of int64 cell IDs at varying levels, ordered as the coverer produced them (sortable but not sorted).

Return type:

list[int]

siege_utilities.geo.s2_spatial_join(points_df, polygons_gdf, lat_col, lon_col, level=12, polygon_id_col=None)[source]

Join points to polygons via S2 cell matching (approximate PiP).

Same shape as h3_spatial_join(). Points and polygons sharing a cell are joined; polygons are decomposed into S2 cells via s2_index_polygon(). First-polygon-wins on overlap.

Parameters:
  • points_df (pd.DataFrame)

  • lat_col (str)

  • lon_col (str)

  • level (int)

  • polygon_id_col (Optional[str])

Return type:

pd.DataFrame

siege_utilities.geo.s2_cell_to_boundary(cell_id)[source]

Return the 4 vertex coordinates of a cell as (lat, lng) tuples.

Accepts an int64 cell ID or a hex token string.

Return type:

list[tuple[float, float]]

siege_utilities.geo.s2_level_for_area(target_area_km2)[source]

Suggest the S2 level whose average cell area matches target_area_km2.

Comparison is in log-space, mirroring h3_resolution_for_area().

Parameters:

target_area_km2 (float)

Return type:

int

siege_utilities.geo.s2_level_for_admin_level(level)[source]

Suggest an S2 level whose average cell area matches a US admin level.

Same admin levels recognised by h3_resolution_for_admin_level() (state / county / zcta / tract / block_group / block, with the usual aliases). Returns the S2 level that’s closest in log-space.

Parameters:

level (str)

Return type:

int

siege_utilities.geo.s2_kring(cell_id, k=1)[source]

Return cell IDs within k steps of cell_id on the same level.

The k-ring is approximate for S2 (squares don’t tile uniformly); we walk neighbors recursively. For exact k-ring on a uniform grid use h3_kring() instead.

Parameters:

k (int)

Return type:

list[int]

siege_utilities.geo.s2_distance(a, b)[source]

Edge-step distance between two S2 cells at the same level.

Counts cell-boundary crossings via repeated neighbor expansion until b is reached. O(distance); use only for small radii. For large distances, switch to great-circle distance on the cell centers.

Return type:

int

siege_utilities.geo.s2_parent(cell_id, level)[source]

Return the ancestor cell at level (must be ≤ current level).

Parameters:

level (int)

Return type:

int

siege_utilities.geo.s2_children(cell_id)[source]

Return the four immediate children of a cell.

Raises if called on a leaf cell (level 30).

Return type:

list[int]

siege_utilities.geo.s2_cell_id_to_uint64(cell_or_token)[source]

Return the int64 form of a cell, accepting either a token or an id.

Useful when receiving a token string (e.g. "88891b") and needing the integer for a database column. Accepts any integer-like type (Python int, numpy.int64, numpy.uint64) for round-trip convenience.

Return type:

int

siege_utilities.geo.s2_uint64_to_cell_id(n)[source]

Return an s2sphere.CellId from an int64 value.

Parameters:

n (int)

siege_utilities.geo.s2_cell_id_to_token(cell_id)[source]

Return the compact hex token form of a cell.

Tokens are stable, JSON-safe, and 16 characters or fewer.

Return type:

str

siege_utilities.geo.s2_token_to_cell_id(token)[source]

Parse a hex token back to its int64 cell ID.

Parameters:

token (str)

Return type:

int

siege_utilities.geo.s2_bbox_to_cells(min_lat, min_lon, max_lat, max_lon, *, max_level=18, max_cells=64)[source]

Cover an axis-aligned bounding box with at most max_cells cells.

Convenience wrapper around s2_region_cover() for the common case of a map-viewport query. Returns int64 cell IDs at varying levels.

Parameters:
Return type:

list[int]

siege_utilities.geo.s2_cells_to_ranges(cell_ids)[source]

Convert cell IDs to (range_min, range_max) pairs for SQL.

Each S2 cell, regardless of level, has a contiguous range of leaf cell IDs that lie within it: [cell.range_min, cell.range_max]. Pre-computing those ranges lets a SQL query test “is point’s cell inside this region” with a simple WHERE leaf_cell_id BETWEEN min AND max (one comparison per cell in the cover, no spatial library needed at query time).

Parameters:

cell_ids (Iterable[int])

Return type:

list[tuple[int, int]]

siege_utilities.geo.index_points(df, lat_col, lon_col, *, grid=None, resolution=None, level=None, engine=None, **kwargs)[source]

Compute a grid cell ID for each row in df.

Dispatches to h3_index_points() or s2_index_points() based on the inference rules. Pass resolution= for H3 (default 8) or level= for S2 (default 12). Extra keyword arguments are forwarded to the underlying function (e.g. as_token=False for S2).

Returns:

pd.Series of cell IDs (H3 hex strings or S2 cell tokens by default).

Parameters:
  • df (Any)

  • lat_col (str)

  • lon_col (str)

  • grid (str | None)

  • resolution (int | None)

  • level (int | None)

  • engine (Any)

  • kwargs (Any)

Return type:

Any

siege_utilities.geo.index_polygon(geometry, *, grid=None, resolution=None, level=None, max_cells=None, min_level=None, max_level=None, refine=True)[source]

Cover a polygon with grid cells.

With H3: returns a set of hex IDs at the requested resolution (uniform).

With S2: if max_cells is provided (or any of min_level, max_level), returns a list of int64 cell IDs from s2_region_cover() (variable-resolution coverage with a cell- count budget). Otherwise returns a set of int64 cell IDs at a single level from s2_index_polygon().

The shape of the return value (set vs list) intentionally signals which mode you got. For SQL-friendly cell-id ranges, see s2_cells_to_ranges().

Parameters:
  • grid (str | None)

  • resolution (int | None)

  • level (int | None)

  • max_cells (int | None)

  • min_level (int | None)

  • max_level (int | None)

  • refine (bool)

Return type:

set | list

siege_utilities.geo.infer_grid(grid, kwargs)[source]

Resolve grid= per the documented inference rules.

Returns "h3" or "s2"; raises ValueError if the inputs contradict the chosen grid. Pure function — does not mutate kwargs.

Parameters:
Return type:

str

class siege_utilities.geo.PlanAuthority[source]

Bases: str, Enum

Who drew the plan — legally meaningful for citation and audit.

LEGISLATURE = 'legislature'

Enacted by the state legislature (the default cycle path).

COURT = 'court'

Court-imposed map (interim or final).

COMMISSION = 'commission'

Independent or politician redistricting commission.

DEFAULT = 'default'

Pre-decennial-shift baseline; used by the legacy resolver when no explicit plan covers a date.

__new__(value)
class siege_utilities.geo.PlanDistrict[source]

Bases: object

A single district within a redistricting plan.

All boundaries fields are optional — many consumers only need the identity (which plan, which district, valid when) and resolve the geometry separately via a BoundaryProvider.

state_fips

2-character state FIPS code (e.g. "01" for Alabama).

Type:

str

district_type

Lowercase district class — "cd", "sldu", "sldl", "county_commission", etc. Match the keys used by CensusTIGERProvider.

Type:

str

district_id

District identifier within the plan. "7" for AL-7, "01" for SLDU 1, etc. Stored as the string form so leading-zero district numbers (sometimes used by states) are preserved.

Type:

str

plan_name

Stable, human-readable plan identifier (e.g. "AL_2023_CD_INTERIM"). Conventional but not enforced.

Type:

str

authority

Who drew the plan (see PlanAuthority).

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the district is in legal effect.

Type:

datetime.date

effective_to

Last date the district is in legal effect, or ``None`` for an open-ended plan (the currently active one).

Type:

datetime.date | None

geometry_source

Optional pointer to the geometry — a URL, a local path, an RDH dataset slug, or any string the consuming BoundaryProvider understands. None is fine if callers only need identity resolution.

Type:

str | None

notes

Free-text annotation (e.g. court docket, statute citation). Goes through to logs and audit trails verbatim.

Type:

str | None

state_fips: str
district_type: str
district_id: str
plan_name: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
geometry_source: str | None = None
notes: str | None = None
covers_date(when)[source]

Return True iff when falls within this district’s effective span.

Half-open at the upper end: effective_to is inclusive (if the new plan starts the next day, encode that as effective_from = old_to + timedelta(days=1)). This matches how court orders are typically written (“effective for elections on or after DATE”) and avoids the off-by-one trap of half-open Python slices.

Parameters:

when (date)

Return type:

bool

__init__(state_fips, district_type, district_id, plan_name, authority, effective_from, effective_to=None, geometry_source=None, notes=None)
Parameters:
  • state_fips (str)

  • district_type (str)

  • district_id (str)

  • plan_name (str)

  • authority (PlanAuthority)

  • effective_from (date)

  • effective_to (date | None)

  • geometry_source (str | None)

  • notes (str | None)

Return type:

None

class siege_utilities.geo.RedistrictingPlan[source]

Bases: object

A full plan: a collection of districts plus plan-level metadata.

The plan is the unit of court orders, statutes, and commission actions; the individual PlanDistrict rows just decompose it for query convenience. effective_from / effective_to on the plan should match the values on its constituent districts.

plan_name

Same convention as PlanDistrict.plan_name.

Type:

str

state_fips

2-character state FIPS.

Type:

str

district_type

"cd", "sldu", etc.

Type:

str

authority

Who drew it.

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the plan is in legal effect.

Type:

datetime.date

effective_to

Last date in effect, or None if currently active.

Type:

datetime.date | None

districts

Tuple of PlanDistrict rows under this plan.

Type:

Tuple[siege_utilities.geo.plans.models.PlanDistrict, …]

metadata

Free-form mapping for citation, source URLs, court docket numbers, etc. Not used for resolution — consumers can stuff whatever they want here.

Type:

Mapping[str, str]

Note: frozen=True only forbids attribute reassignment; the metadata dict remains mutable in place. Instances are therefore not hashable and should be treated as value objects, never used as dict keys or set members.

plan_name: str
state_fips: str
district_type: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
districts: Tuple[PlanDistrict, ...]
metadata: Mapping[str, str]
covers_date(when)[source]

Return True iff when falls within this plan’s effective span.

Parameters:

when (date)

Return type:

bool

district(district_id)[source]

Find a district within this plan by id, or None.

Parameters:

district_id (str)

Return type:

PlanDistrict | None

__init__(plan_name, state_fips, district_type, authority, effective_from, effective_to=None, districts=<factory>, metadata=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.PlanRegistry[source]

Bases: object

Stores redistricting plans and resolves them by date.

Plans are keyed by (state_fips, district_type). Within a key, plans should be temporally non-overlapping; the registry detects overlap at registration time (warn) and at resolution time (raise in strict mode).

Thread-safe for concurrent readers; mutation under a lock.

__init__()[source]
Return type:

None

register_plan(plan)[source]

Add plan to the registry.

Logs a warning if it overlaps an already-registered plan for the same (state_fips, district_type). Does not raise on overlap — registration is forgiving so consumers can load partial data; resolution is the strict gate.

Parameters:

plan (RedistrictingPlan)

Return type:

None

register_plans(plans)[source]

Bulk-register an iterable of plans.

Parameters:

plans (Iterable[RedistrictingPlan])

Return type:

None

clear()[source]

Remove all registered plans (mostly for tests).

Return type:

None

plans_for_state(state_fips, district_type)[source]

Return all registered plans for state_fips / district_type.

Sorted by effective_from ascending. Empty list if none.

Parameters:
  • state_fips (str)

  • district_type (str)

Return type:

List[RedistrictingPlan]

resolve_plan_at_date(state_fips, district_type, when, *, strict=True)[source]

Find the plan in effect for state_fips / district_type on when.

Parameters:
  • state_fips (str) – 2-char state FIPS.

  • district_type (str) – "cd", "sldu", "sldl", etc.

  • when (date) – The date to resolve.

  • strict (bool) – If True (default), raise PlanOverlapError when more than one plan covers when. If False, log a warning and return the most recently enacted one (latest effective_from) — appropriate when court interim/final pairs both technically cover the same day.

Raises:
Return type:

RedistrictingPlan

resolve_district_at_date(state_fips, district_type, district_id, when, *, strict=True)[source]

Find the specific district covering when.

Convenience wrapper: resolves the plan, then looks up the district by id. Raises PlanResolutionError if the plan does not contain district_id.

Parameters:
Return type:

PlanDistrict

exception siege_utilities.geo.PlanResolutionError[source]

Bases: LookupError

No plan covers the requested (state, district_type, date) tuple.

exception siege_utilities.geo.PlanOverlapError[source]

Bases: ValueError

Two registered plans cover the same date for the same state+type.

Raised by PlanRegistry.resolve_plan_at_date() when the registry finds more than one match in strict mode. Caller can rerun with strict=False to get the first match (and a warning), or fix the underlying plan data.

siege_utilities.geo.get_default_plan_registry()[source]

Return the process-global default PlanRegistry.

Lazily instantiated on first call. Consumers that need isolation (multi-tenant code, unit tests) should instantiate PlanRegistry directly instead.

Return type:

PlanRegistry

siege_utilities.geo.detect_format(file_path)[source]

Return a format token for a vector GIS file.

Parameters:

file_path (str) – Path to a vector GIS file on disk.

Returns:

"shapefile", "geojson", "kml", "kmz", "geopackage", "dxf", "swmaps", "unknown".

Return type:

One of

Raises:

FileNotFoundError – If the file does not exist.

Detection algorithm:

  1. Extension lookup (case-insensitive) against a small canonical map.

  2. For ambiguous extensions (.json, .sqlite, .db, .zip), a magic-byte / archive-peek fallback inspects the file body.

Directories return "unknown" — walking is the caller’s responsibility.

siege_utilities.geo.extract_attributes_ogr(feature, field_names, missing='none')[source]

Extract one attribute from a fiona-record-shaped feature.

Mirrors the candidate-list semantics of the salvage-source helper _getOGRDataFromSource (DataUtils.py:489-513) which accepts a primary plus a fallback field name and returns None when neither is present. The salvage source used the GDAL/OGR Python bindings directly; this implementation is fiona-record-shaped (the high-level OGR wrapper that SU declares as a geo dep).

Parameters:
  • feature (Any) – A fiona-style record ({"properties": {...}, ...}) OR a plain dict (e.g. the property dict directly).

  • field_names (Iterable[str]) – Candidate field names, in priority order. First name present on the feature wins.

  • missing (str) – One of "none" (default; return None), "raise" (KeyError), or "warn" (log + return None).

Returns:

The attribute value or None.

Return type:

Any

siege_utilities.geo.extract_attributes_kml(kml_extended_data, field_names, missing='none')[source]

Extract one attribute from a KML extended-data mapping.

Mirrors the salvage-source helper _getKMLDataFromSource (DataUtils.py:516-520). The salvage version was a one-liner dict lookup with no candidate fallback; this version adds the same candidate-list semantics as extract_attributes_ogr() for consistency across the dispatcher.

Parameters:
  • kml_extended_data (dict | None) – A flat {name: value} mapping derived from a KML <ExtendedData> block.

  • field_names (Iterable[str]) – Candidate field names, in priority order.

  • missing (str) – "none" / "raise" / "warn".

Returns:

The attribute value or None.

Return type:

Any

siege_utilities.geo.extract_attributes_swmaps(db_path, feature_id, field_names, missing='none')[source]

Extract one attribute for a SWMaps feature by attribute-field name.

Queries the SWMaps SQLite attribute_values table joined to attribute_fields for the named field, scoped to feature_id. This is the same join pattern as the salvage source’s swmapsAttributeQuery (DataUtils.py:693-698); the application-schema-specific mapper (swMapsAttributeMapper dict at DataUtils.py:672-686) is NOT lifted — that lives with the consumer.

For full SWMaps feature iteration (geometry + all attributes), use siege_utilities.geo.swmaps_reader instead; this function is the thin dispatcher-friendly accessor.

Parameters:
  • db_path (str) – Path to a SWMaps SQLite file (already extracted from .swmz if needed; archive handling lives in the swmaps_reader module).

  • feature_id (str) – SWMaps feature UUID.

  • field_names (Iterable[str]) – Candidate attribute-field names.

  • missing (str) – "none" / "raise" / "warn".

Returns:

The attribute value (as stored in attribute_values.value) or None.

Return type:

Any

class siege_utilities.geo.TemporalDataStore[source]

Bases: object

Pure-Python persistence for temporal geographic data.

Follows the CrosswalkClient pattern: class with module-level convenience functions for the common case.

SUPPORTED_FORMATS = ('parquet', 'gpkg')
__init__(root_dir=None, format='parquet')[source]
Parameters:
Return type:

None

save_boundaries(gdf, geography_type, vintage_year)[source]

Persist a GeoDataFrame of boundaries.

Parameters:
  • gdf (gpd.GeoDataFrame)

  • geography_type (str)

  • vintage_year (int)

Return type:

Path

load_boundaries(geography_type, vintage_year, state_fips=None)[source]

Load boundaries for a geography type and vintage year.

Parameters:
  • geography_type (str)

  • vintage_year (int)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

query_boundaries_at_date(geography_type, query_date, state_fips=None)[source]

Load the best-matching vintage for a given date.

If boundaries have valid_from/valid_to columns, those are checked. Otherwise, the nearest vintage_year <= query_date.year is used.

Parameters:
  • geography_type (str)

  • query_date (date)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

list_available_vintages(geography_type)[source]

List vintage years available on disk for a geography type.

Parameters:

geography_type (str)

Return type:

list[int]

save_demographics(snapshots, geography_type, dataset='acs5', year=None)[source]

Persist demographic snapshot data as Parquet.

Parameters:
  • snapshots (pd.DataFrame)

  • geography_type (str)

  • dataset (str)

  • year (int | None)

Return type:

Path

load_demographics(geography_type, year=None, dataset='acs5')[source]

Load demographic data. If year is None, loads all available years.

Parameters:
  • geography_type (str)

  • year (int | None)

  • dataset (str)

Return type:

pd.DataFrame

save_timeseries(series, geography_type, variable_code=None, dataset='acs5')[source]

Persist pre-computed time-series data as Parquet.

Parameters:
  • series (pd.DataFrame)

  • geography_type (str)

  • variable_code (str | None)

  • dataset (str)

Return type:

Path

load_timeseries(geography_type, variable_code, dataset='acs5')[source]

Load pre-computed time-series data.

Parameters:
  • geography_type (str)

  • variable_code (str)

  • dataset (str)

Return type:

pd.DataFrame

siege_utilities.geo.get_temporal_store(root_dir=None, format='parquet')[source]

Get or create the singleton TemporalDataStore.

Parameters:
Return type:

TemporalDataStore

siege_utilities.geo.save_boundaries(gdf, geography_type, vintage_year, **kwargs)[source]

Save boundaries via the default store.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame of boundaries to persist.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.load_boundaries(geography_type, vintage_year, state_fips=None, **kwargs)[source]

Load boundaries via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.query_boundaries_at_date(geography_type, query_date, state_fips=None, **kwargs)[source]

Query boundaries at a date via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • query_date (date) – Date for which to find matching boundaries.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.save_demographics(snapshots, geography_type, dataset='acs5', year=None, **kwargs)[source]

Save demographics via the default store.

Parameters:
  • snapshots (pd.DataFrame) – DataFrame of demographic snapshot data.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • dataset (str) – Dataset identifier (default “acs5”).

  • year (int | None) – Year of the snapshot. Inferred from DataFrame if omitted.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.load_demographics(geography_type, year=None, dataset='acs5', **kwargs)[source]

Load demographics via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • year (int | None) – Year to load. If None, loads all available years.

  • dataset (str) – Dataset identifier (default “acs5”).

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

pd.DataFrame

siege_utilities.geo.spatial_query(boundaries, points, predicate='intersects', *, crs=None)[source]

Spatial join between boundaries and points using geopandas.sjoin.

Aligns CRS before joining if both GeoDataFrames have a CRS set.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons (left)

  • points (gpd.GeoDataFrame) – GeoDataFrame of query geometries (right)

  • predicate (str) – Spatial predicate (intersects, within, contains, etc.)

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

Joined GeoDataFrame with columns from both inputs, in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal_filter(gdf, query_date, valid_from_col='valid_from', valid_to_col='valid_to', vintage_year_col='vintage_year')[source]

Filter a GeoDataFrame to rows valid at query_date.

First tries valid_from/valid_to date range filtering. Falls back to vintage_year if date columns are missing or all-null.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame with temporal columns

  • query_date (date) – Date to query for

  • valid_from_col (str) – Column name for validity start date

  • valid_to_col (str) – Column name for validity end date

  • vintage_year_col (str | None) – Fallback column for nearest-year matching

Returns:

Filtered GeoDataFrame

Return type:

gpd.GeoDataFrame

siege_utilities.geo.point_in_boundary(boundaries, lon, lat, predicate='intersects', *, crs=None)[source]

Find which boundaries contain a single point.

Convenience wrapper that creates a Point GeoDataFrame and calls spatial_query.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons

  • lon (float) – Longitude

  • lat (float) – Latitude

  • predicate (str) – Spatial predicate

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

GeoDataFrame of matching boundaries in crs.

Return type:

gpd.GeoDataFrame

Core Spatial Modules

Modern spatial data sources for siege_utilities. Provides clean, type-safe access to Census, Government, and OpenStreetMap data.

exception siege_utilities.geo.spatial_data.SpatialDataError[source]

Bases: RuntimeError

Raised when a non-boundary spatial data fetch fails unexpectedly.

Used by GovernmentDataSource and OpenStreetMapDataSource, which load generic portal datasets and OSM Overpass results respectively. Boundary retrieval has its own exception hierarchy in boundary_result.py.

Use the __cause__ attribute (set via raise ... from e) to inspect the underlying exception (HTTPError, JSONDecodeError, etc.).

class siege_utilities.geo.spatial_data.SpatialDataSource[source]

Bases: object

Base class for spatial data sources.

__init__(name, base_url, api_key=None)[source]

Initialize spatial data source.

Parameters:
  • name (str) – Name of the data source

  • base_url (str) – Base URL for the data source

  • api_key (str | None) – API key if required

download_data(**kwargs)[source]

Download data from the source.

Return type:

geopandas.GeoDataFrame | None

class siege_utilities.geo.spatial_data.CensusDataSource[source]

Bases: SpatialDataSource

Census TIGER/Line boundary data source with dynamic year discovery.

Supports all standard TIGER/Line boundary types:

National-scope (no state_fips required):

nation, state, county, place, zcta, cd

State-scope (state_fips required):

tract, block_group, block, tabblock20, sldu, sldl

GEOID formats by boundary type:

Boundary

Digits

Components

state

2

SS

county

5

SS + CCC

tract

11

SS + CCC + TTTTTT

block_group

12

SS + CCC + TTTTTT + G

tabblock20

15

SS + CCC + TTTTTT + BBBB

cd

4

SS + DD

sldu

5

SS + DDD

sldl

5

SS + DDD

__init__(api_key=None, timeout=None)[source]

Initialize spatial data source.

Parameters:
  • name – Name of the data source

  • base_url – Base URL for the data source

  • api_key (str | None) – API key if required

  • timeout (int | None)

get_available_years(force_refresh=False)[source]

Get list of available Census years.

Parameters:

force_refresh (bool) – If True, re-discover years from Census server

Returns:

List of available years

Return type:

List[int]

get_year_directory_contents(year)[source]

Get directory contents for a specific year.

Parameters:

year (int)

Return type:

List[str]

discover_boundary_types(year)[source]

Discover available boundary types for a year.

Returns a mapping of boundary-type slug ('county', 'tract', …) to the canonical TIGER directory name ('COUNTY', 'TRACT', …). Matches the return contract of TigerDiscovery.discover_boundary_types(). The previous List[str] annotation was incorrect – the underlying call has always returned the dict.

Parameters:

year (int)

Return type:

Dict[str, str]

get_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, county_fips=None, congress_number=None)[source]

Download geographic boundaries from Census TIGER/Line files with dynamic discovery.

This method preserves the legacy Optional[GeoDataFrame] return type. For structured diagnostics, use fetch_geographic_boundaries() instead.

Parameters:
  • year (int) – Census year (will find closest available if not available)

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code for filtering (required for some levels)

  • county_fips (str | None) – County FIPS code for filtering

  • congress_number (int | None) – Congress number for congressional districts

Returns:

GeoDataFrame with boundaries or None if failed

Return type:

geopandas.GeoDataFrame | None

fetch_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, county_fips=None, congress_number=None)[source]

Download geographic boundaries with structured diagnostics.

Returns a BoundaryFetchResult that carries either the GeoDataFrame on success or a machine-readable error code, stage, and context dict on failure.

Parameters:
  • year (int) – Census year (will find closest available if not available)

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code for filtering (required for some levels)

  • county_fips (str | None) – County FIPS code for filtering

  • congress_number (int | None) – Congress number for congressional districts

Returns:

BoundaryFetchResult with .success, .geodataframe, .error_stage, etc.

Return type:

BoundaryFetchResult

get_available_boundary_types(year)[source]

Get available boundary types for a specific year.

Parameters:

year (int)

Return type:

Dict[str, str]

refresh_discovery_cache()[source]

Refresh the discovery cache to get latest available data.

get_available_state_fips()[source]

Get mapping of state FIPS codes to state names.

Return type:

Dict[str, str]

get_state_abbreviations()[source]

Get mapping of state FIPS codes to state abbreviations.

Return type:

Dict[str, str]

normalize_state_identifier(state_input)[source]

Convert any state identifier (FIPS, abbreviation, name) to FIPS code.

Deprecated since version 3.16.0: Use the module-level normalize_state_identifier() directly.

Raises:

ValueError – If the input cannot be resolved to a valid state FIPS.

Return type:

str

get_comprehensive_state_info()[source]

Get comprehensive state information including FIPS, name, and abbreviation.

Return type:

Dict[str, Dict[str, str]]

get_state_by_abbreviation(abbreviation)[source]

Get state information by abbreviation.

Parameters:

abbreviation (str)

Return type:

Dict[str, str] | None

get_state_by_name(name)[source]

Get state information by name (case-insensitive partial match).

Parameters:

name (str)

Return type:

Dict[str, str] | None

validate_state_fips(state_fips)[source]

Validate if a state FIPS code is valid.

Parameters:

state_fips (str)

Return type:

bool

get_state_name(state_fips)[source]

Get state name from FIPS code.

Parameters:

state_fips (str)

Return type:

str | None

get_state_abbreviation(state_fips)[source]

Get state abbreviation from FIPS code.

Parameters:

state_fips (str)

Return type:

str | None

class siege_utilities.geo.spatial_data.GovernmentDataSource[source]

Bases: SpatialDataSource

Government data portal spatial data source.

__init__(portal_url, api_key=None)[source]

Initialize government data source.

Parameters:
  • portal_url (str) – Base URL for the government data portal

  • api_key (str | None) – API key if required

download_dataset(dataset_id, format_type='geojson')[source]

Download a dataset from the government data portal.

Parameters:
  • dataset_id (str) – Unique identifier for the dataset

  • format_type (str) – Preferred format (geojson, shapefile, kml)

Returns:

GeoDataFrame with spatial data.

Raises:

SpatialDataError – On any download or processing failure.

Return type:

geopandas.GeoDataFrame

class siege_utilities.geo.spatial_data.OpenStreetMapDataSource[source]

Bases: SpatialDataSource

OpenStreetMap data source using Overpass API.

__init__()[source]

Initialize OpenStreetMap data source.

download_osm_data(query, bbox=None)[source]

Download data from OpenStreetMap using Overpass QL.

Parameters:
  • query (str) – Overpass QL query string

  • bbox (List[float] | None) – Bounding box [min_lat, min_lon, max_lat, max_lon]

Returns:

GeoDataFrame with OSM data.

Raises:

SpatialDataError – On HTTP or processing failure.

Return type:

geopandas.GeoDataFrame

class siege_utilities.geo.spatial_data.CensusDirectoryDiscovery[source]

Bases: object

Discovers available Census TIGER/Line data dynamically.

__init__(timeout=None)[source]
Parameters:

timeout (int | None)

get_available_years(force_refresh=False)[source]

Get list of available Census years with retry and exponential backoff.

Parameters:

force_refresh (bool)

Return type:

List[int]

get_year_directory_contents(year, force_refresh=False, on_error='skip')[source]

Get contents of a specific TIGER year directory.

Parameters:
  • year (int) – Census TIGER year.

  • force_refresh (bool) – Bypass the in-memory cache.

  • on_error ("raise" | "warn" | "skip") – What to do when the network request fails. "skip" (default) returns an empty list silently; "warn" logs and returns empty; "raise" propagates the SiegeGeoError.

Return type:

List[str]

discover_boundary_types(year)[source]

Discover what boundary types are available for a given year.

Parameters:

year (int)

Return type:

Dict[str, str]

get_year_specific_url_patterns(year)[source]

Get year-specific URL patterns and directory structures.

Parameters:

year (int)

Return type:

Dict[str, Any]

construct_download_url(year, boundary_type, state_fips=None, congress_number=None)[source]

Construct download URL using FIPS validation and year-specific patterns.

Raises:
Parameters:
  • year (int)

  • boundary_type (str)

  • state_fips (str | None)

  • congress_number (int | None)

Return type:

str

validate_download_url(url)[source]

Check if a download URL is actually accessible.

Uses GET with stream=True instead of HEAD because CDNs like Cloudflare often block or throttle HEAD requests from non-browser User-Agents.

Raises:

BoundaryUrlValidationError – If the URL is not accessible.

Parameters:

url (str)

Return type:

bool

get_optimal_year(requested_year, boundary_type)[source]

Find the best available year for a requested boundary type.

Parameters:
  • requested_year (int)

  • boundary_type (str)

Return type:

int

siege_utilities.geo.spatial_data.get_census_data(api_key=None)[source]

Get Census data source instance.

Parameters:

api_key (str | None)

Return type:

CensusDataSource

siege_utilities.geo.spatial_data.get_census_boundaries(year=2026, geographic_level='county', state_fips=None, state_identifier=None)[source]

Convenience function to get Census boundaries.

Deprecated since version 3.16.0: Use fetch_geographic_boundaries() (via CensusDataSource) for structured diagnostics via BoundaryFetchResult. get_census_boundaries will be removed in v3.17.0.

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (county, tract, etc.)

  • state_fips (str | None) – State FIPS code (e.g., ‘06’ for California)

  • state_identifier (str | None) – State identifier - accepts FIPS code, abbreviation, or name (e.g., ‘06’, ‘CA’, ‘California’ all work)

Return type:

geopandas.GeoDataFrame | None

Note

Provide either state_fips OR state_identifier, not both. If both are provided, state_identifier takes precedence.

Returns:

GeoDataFrame with Census boundaries (filtered by state if specified)

Parameters:
  • year (int)

  • geographic_level (str)

  • state_fips (str | None)

  • state_identifier (str | None)

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.spatial_data.get_available_years(force_refresh=False)[source]

Get available Census years.

Parameters:

force_refresh (bool)

Return type:

List[int]

siege_utilities.geo.spatial_data.get_year_directory_contents(year)[source]

Get directory contents for a specific year.

Parameters:

year (int)

Return type:

List[str]

siege_utilities.geo.spatial_data.discover_boundary_types(year)[source]

Discover available boundary types for a year.

Returns the same {boundary_type: tiger_dir} mapping as CensusDataSource.discover_boundary_types(). The historical List[str] annotation never matched runtime behavior.

Parameters:

year (int)

Return type:

Dict[str, str]

siege_utilities.geo.spatial_data.download_data(year, geographic_level, state_fips=None, *, crs=None)[source]

Download Census data.

This is a convenience wrapper around get_geographic_boundaries().

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code (required for tract, block_group, etc.)

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

GeoDataFrame with boundaries in crs, or None if failed.

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.spatial_data.download_dataset(year, geographic_level, state_fips=None, *, crs=None)[source]

Download Census dataset.

Parameters:
  • year (int) – Census year.

  • geographic_level (str) – Geographic level.

  • state_fips (str | None) – State FIPS code.

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.spatial_data.get_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, state_identifier=None, *, crs=None)[source]

Get geographic boundaries (legacy, returns None on failure).

Deprecated since version 3.16.0: Use fetch_geographic_boundaries() for structured diagnostics. get_geographic_boundaries will be removed in v3.17.0.

Parameters:
Return type:

geopandas.GeoDataFrame | None

siege_utilities.geo.spatial_data.fetch_geographic_boundaries(year=2026, geographic_level='county', state_fips=None, congress_number=None, *, crs=None)[source]

Get geographic boundaries with structured diagnostics.

Returns a BoundaryFetchResult instead of Optional[GeoDataFrame].

Parameters:
  • year (int) – Census year

  • geographic_level (str) – Geographic level (state, county, tract, etc.)

  • state_fips (str | None) – State FIPS code (required for tract, block_group, etc.)

  • congress_number (int | None) – Congress number for congressional districts

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

BoundaryFetchResult with .success, .geodataframe in crs, .error_stage, etc.

Return type:

BoundaryFetchResult

siege_utilities.geo.spatial_data.load_census_inventory(path=None)[source]

Load the saved Census inventory from disk. Returns None if absent.

Raises:

SpatialDataError – If the file exists but cannot be parsed.

Parameters:

path (Path | None)

Return type:

dict | None

siege_utilities.geo.spatial_data.update_census_inventory(years=None, include_genz=True, save_path=None, request_delay=0.5, timeout=15)[source]

Crawl the Census FTP and return a timestamped directory inventory.

Fetches the TIGER (and optionally GENZ) year→layers structure using BeautifulSoup and saves it as JSON so callers can use it as a fallback when live Census requests are blocked (403) or rate-limited (429).

Parameters:
  • years (List[int] | None) – Specific years to crawl. Defaults to 2010 through the current year.

  • include_genz (bool) – Also crawl the Cartographic Boundary File (GENZ) tree.

  • save_path (Path | None) – Where to save the JSON. Defaults to ~/.cache/siege_utilities/census_inventory.json.

  • request_delay (float) – Seconds to sleep between HTTP requests to avoid hammering the Census server.

  • timeout (int) – Per-request timeout in seconds.

Returns:

  • dict with keys updated_at, tiger, and (optionally) genz.

  • tiger[year] is a sorted list of layer directory names.

  • genz[year] is a sorted list of available CB file name stems.

Return type:

dict

siege_utilities.geo.spatial_data.refresh_discovery_cache()[source]

Refresh the discovery cache.

Return type:

None

siege_utilities.geo.spatial_data.construct_download_url(year, geographic_level, state_fips=None)[source]

Construct download URL for Census data.

Parameters:
  • year (int)

  • geographic_level (str)

  • state_fips (str | None)

Return type:

str

siege_utilities.geo.spatial_data.validate_download_url(url)[source]

Validate a download URL.

Parameters:

url (str)

Return type:

bool

siege_utilities.geo.spatial_data.get_optimal_year(geographic_level, preferred_year=None)[source]

Get optimal year for geographic level.

Parameters:
  • geographic_level (str) – Geographic level (county, tract, etc.)

  • preferred_year (int | None) – Preferred year (defaults to current year if None)

Returns:

Best available year for the requested boundary type

Return type:

int

siege_utilities.geo.spatial_data.get_available_boundary_types(year)[source]

Get available boundary types for a year.

Returns {boundary_type: tiger_dir} – same shape as CensusDataSource.get_available_boundary_types(). The historical List[str] annotation never matched runtime behavior.

Parameters:

year (int)

Return type:

Dict[str, str]

siege_utilities.geo.spatial_data.normalize_state_identifier(state_input)[source]

Normalize a state identifier to its 2-digit zero-padded FIPS code.

Parameters:

state_input (str) – Any of: 2-digit FIPS code ("48"), 2-letter abbreviation ("TX"), or full state name ("Texas"). Case-insensitive.

Returns:

Zero-padded 2-digit FIPS code (e.g., "06" for California).

Return type:

str

Raises:

ValueError – If state_input doesn’t match any known state, territory, or DC.

Examples

>>> normalize_state_identifier("TX")
'48'
>>> normalize_state_identifier("48")
'48'
>>> normalize_state_identifier("Texas")
'48'
>>> normalize_state_identifier("DC")
'11'
siege_utilities.geo.spatial_data.normalize_state_identifier_standalone(identifier)[source]

Normalize a state identifier to its 2-digit FIPS code.

Delegates to siege_utilities.config.census_registry.normalize_state_identifier(), the canonical implementation.

Parameters:

identifier (str) – Any of: 2-digit FIPS code, 2-letter abbreviation, or full state name.

Returns:

Zero-padded 2-digit FIPS code.

Return type:

str

Raises:

ValueError – If identifier doesn’t match any known state, territory, or DC.

siege_utilities.geo.spatial_data.normalize_state_input(raw_input)[source]

Normalize state input to handle formatting and trivial errors.

Parameters:

raw_input (str) – Raw state input (name, abbreviation, or FIPS)

Returns:

Normalized input (uppercase, trimmed, FIPS padded)

Return type:

str

Examples

normalize_state_input(” ca “) -> “CA” normalize_state_input(“CAlifornia”) -> “CALIFORNIA” normalize_state_input(“6”) -> “06”

siege_utilities.geo.spatial_data.normalize_state_name(name)[source]

Normalize state name input (e.g., “CAlifornia” -> “CALIFORNIA”).

Parameters:

name (str) – State name input

Returns:

Normalized state name in uppercase

Return type:

str

siege_utilities.geo.spatial_data.normalize_state_abbreviation(abbrev)[source]

Normalize state abbreviation input (e.g., “ ca “ -> “CA”).

Parameters:

abbrev (str) – State abbreviation input

Returns:

Normalized state abbreviation in uppercase

Return type:

str

siege_utilities.geo.spatial_data.normalize_fips_code(fips)[source]

Normalize FIPS code input (e.g., 6 -> “06”, “6” -> “06”).

Parameters:

fips (str | int) – FIPS code as string or integer

Returns:

Normalized FIPS code as 2-digit string

Return type:

str

siege_utilities.geo.spatial_data.get_state_by_abbreviation(abbreviation)[source]

Get state info by abbreviation.

Parameters:

abbreviation (str)

Return type:

Dict[str, Any] | None

siege_utilities.geo.spatial_data.get_state_by_name(name)[source]

Get state info by name.

Parameters:

name (str)

Return type:

Dict[str, Any] | None

siege_utilities.geo.spatial_data.get_state_name(fips)[source]

Get state name from FIPS code.

Parameters:

fips (str)

Return type:

str | None

siege_utilities.geo.spatial_data.get_state_abbreviation(fips)[source]

Get state abbreviation from FIPS code.

Parameters:

fips (str)

Return type:

str | None

siege_utilities.geo.spatial_data.get_state_abbreviations()[source]

Get a {fips_code: state_abbreviation} mapping.

Same shape as CensusDataSource.get_state_abbreviations(). Historical List[str] annotation never matched runtime behavior.

Return type:

Dict[str, str]

siege_utilities.geo.spatial_data.get_available_state_fips()[source]

Get available state FIPS codes as a {fips_code: state_name} mapping.

Same shape as CensusDataSource.get_available_state_fips(). Historical List[str] annotation never matched runtime behavior.

Return type:

Dict[str, str]

siege_utilities.geo.spatial_data.get_unified_fips_data()[source]

Get unified FIPS data with state names and abbreviations.

Return type:

Dict[str, Dict[str, Any]]

siege_utilities.geo.spatial_data.get_comprehensive_state_info()[source]

Get comprehensive state information.

Return type:

Dict[str, Dict[str, Any]]

siege_utilities.geo.spatial_data.validate_state_fips(fips)[source]

Validate state FIPS code.

Parameters:

fips (str)

Return type:

bool

siege_utilities.geo.spatial_data.download_osm_data(query, bbox=None, *, crs=None)[source]

Convenience function to download OSM data.

Parameters:
  • query (str) – OSM query string.

  • bbox (List[float] | None) – Optional bounding box [west, south, east, north].

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Return type:

geopandas.GeoDataFrame | None

Spatial data transformation utilities for siege_utilities. Provides format conversion and transformation capabilities using the core geospatial stack. DuckDB support is included but optional for enhanced performance on large datasets.

exception siege_utilities.geo.spatial_transformations.SpatialQueryError[source]

Bases: RuntimeError

Raised when a spatial SQL query fails.

Use the __cause__ attribute (set via raise ... from e) to inspect the underlying database exception.

class siege_utilities.geo.spatial_transformations.SpatialDataTransformer[source]

Bases: object

Transform spatial data between different formats and coordinate systems.

__init__()[source]

Initialize the spatial data transformer.

convert_format(gdf, output_format, **kwargs)[source]

Convert GeoDataFrame to different format.

Parameters:
  • gdf (None) – Input GeoDataFrame

  • output_format (str) – Desired output format

  • **kwargs – Additional format-specific parameters

Raises:
Return type:

None

class siege_utilities.geo.spatial_transformations.PostGISConnector[source]

Bases: object

Handles PostGIS database connections and operations.

__init__(connection_string=None)[source]

Initialize PostGIS connector.

Parameters:

connection_string (str | None) – PostgreSQL connection string. When omitted, we look up the configured postgresql connection from the user-config; if that’s unavailable too, the connector stays uninitialized (self.connection is None).

close()[source]

Close the database connection.

upload_spatial_data(gdf, table_name, **kwargs)[source]

Upload spatial data to PostGIS with all columns preserved.

Uses geopandas.GeoDataFrame.to_postgis under the hood, which handles full-column writes (geometry + every attribute column) via SQLAlchemy + GeoAlchemy2. This is the canonical geopandas idiom for PostGIS upload.

Pre-#516 behavior (replaced): the old implementation manually built INSERT INTO {table} (geom) VALUES (…) statements, dropping all tabular columns from the GeoDataFrame. Any caller passing attribute data got an empty-attribute table silently.

Parameters:
  • gdf (None) – GeoDataFrame to upload (all columns including geometry).

  • table_name (str) – Target table name.

  • **kwargs

    Additional parameters: if_exists: How to handle existing table – 'fail',

    'replace' (default), or 'append'.

    schema: PostgreSQL schema name (default 'public').

Raises:
Return type:

None

download_spatial_data(table_name, *, crs=None, **kwargs)[source]

Download spatial data from PostGIS.

Parameters:
  • table_name (str) – Source table name

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

  • **kwargs – Additional parameters. Accepts geom_col (default 'geom') to specify the geometry column name.

Returns:

GeoDataFrame with spatial data.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails.

Return type:

None

execute_spatial_query(query, *, crs=None, **kwargs)[source]

Execute a spatial SQL query.

For SELECT queries the result is a GeoDataFrame. For non-SELECT queries (INSERT, UPDATE, DELETE, DDL) the result is the cursor.rowcount integer (-1 when the row count is not determinable, e.g. DDL).

Parameters:
  • query (str) – SQL query string

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

  • **kwargs – Additional parameters

Returns:

GeoDataFrame for SELECT queries, int rowcount for others.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails. The original exception is chained as __cause__.

Return type:

None | int

class siege_utilities.geo.spatial_transformations.DuckDBConnector[source]

Bases: object

Handles DuckDB database connections and operations (optional).

__init__(db_path=None)[source]

Initialize DuckDB connector.

Parameters:

db_path (str | None) – Path to DuckDB database file (optional, uses user config if not provided)

connect()[source]

Establish database connection.

Raises:

RuntimeError – If connection fails

Return type:

None

close()[source]

Close the database connection.

upload_spatial_data(gdf, table_name, **kwargs)[source]

Upload spatial data to DuckDB.

Parameters:
  • gdf (None) – GeoDataFrame to upload

  • table_name (str) – Target table name

  • **kwargs – Additional parameters

Raises:

RuntimeError – If connection or upload fails

Return type:

None

download_spatial_data(table_name, *, crs=None, **kwargs)[source]

Download spatial data from DuckDB.

Parameters:
  • table_name (str) – Source table name

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

  • **kwargs – Additional parameters

Returns:

GeoDataFrame with spatial data. WKT geometries stored in DuckDB lack SRID metadata; EPSG:4326 is assumed as the source CRS. Pass crs to reproject to a different target.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails.

Return type:

None

execute_spatial_query(query, *, crs=None, **kwargs)[source]

Execute a spatial SQL query.

For SELECT queries the result is a GeoDataFrame. For non-SELECT queries (INSERT, UPDATE, DELETE, DDL) the result is the cursor.rowcount integer (-1 when the row count is not determinable, e.g. DDL).

Parameters:
  • query (str) – SQL query string

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

  • **kwargs – Additional parameters

Returns:

GeoDataFrame for SELECT queries, int rowcount for others.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails. The original exception is chained as __cause__.

Return type:

None | int

siege_utilities.geo.spatial_transformations.convert_spatial_format(gdf, output_format, **kwargs)[source]

Convert spatial data to different format.

Parameters:
  • gdf (None) – Input GeoDataFrame.

  • output_format (str) – Target format ('shp', 'geojson', 'gpkg', 'kml', 'gml', 'wkt', 'wkb', 'postgis', 'duckdb').

  • **kwargs

    Format-specific parameters: output_path (str): Output file path (default varies by format). db_path (str): DuckDB database path ('duckdb' format only). table_name (str): DuckDB table name ('duckdb' format only,

    default 'spatial_data').

Raises:
  • ValueError – If output_format is unsupported.

  • ImportError – If a required dependency is not available.

Return type:

None

siege_utilities.geo.spatial_transformations.upload_to_postgis(gdf, table_name, connection_string=None, **kwargs)[source]

Upload spatial data to PostGIS.

Parameters:
  • gdf (None) – GeoDataFrame to upload.

  • table_name (str) – Target table name.

  • connection_string (str | None) – PostgreSQL connection string (uses user config when omitted).

  • **kwargs – Forwarded to PostGISConnector.upload_spatial_data(). Accepts if_exists ('fail', 'replace', 'append'; default 'replace') and schema (default 'public').

Raises:
Return type:

None

siege_utilities.geo.spatial_transformations.download_from_postgis(table_name, connection_string=None, **kwargs)[source]

Download spatial data from PostGIS.

Parameters:
  • table_name (str) – Source table name (may be schema-qualified, e.g. 'public.my_table').

  • connection_string (str | None) – PostgreSQL connection string (uses user config when omitted).

  • **kwargs – Forwarded to PostGISConnector.download_spatial_data(). Accepts geom_col (default 'geom') and crs (output CRS).

Returns:

GeoDataFrame with spatial data.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails.

Return type:

None

siege_utilities.geo.spatial_transformations.execute_postgis_query(query, connection_string=None, **kwargs)[source]

Execute a spatial SQL query on PostGIS.

Parameters:
  • query (str) – SQL query string.

  • connection_string (str | None) – PostgreSQL connection string (uses user config when omitted).

  • **kwargs – Forwarded to PostGISConnector.execute_spatial_query(). Accepts geom_col (default 'geometry') and crs (output CRS, applies to SELECT queries only).

Returns:

GeoDataFrame for SELECT queries, int rowcount for others.

Raises:

SpatialQueryError – If the connection is unavailable or the query fails.

Return type:

None | int

siege_utilities.geo.spatial_transformations.upload_to_duckdb(gdf, table_name, db_path=None, **kwargs)[source]

Upload spatial data to DuckDB (optional).

Parameters:
  • gdf (None) – GeoDataFrame to upload.

  • table_name (str) – Target table name.

  • db_path (str | None) – Path to DuckDB database file (uses user config or in-memory when omitted).

  • **kwargs – Forwarded to DuckDBConnector.upload_spatial_data(). Accepts if_exists ('replace' by default).

Raises:
Return type:

None

siege_utilities.geo.spatial_transformations.download_from_duckdb(table_name, db_path=None, **kwargs)[source]

Download spatial data from DuckDB (optional).

Parameters:
  • table_name (str) – Source table name.

  • db_path (str | None) – Path to DuckDB database file (uses user config or in-memory when omitted).

  • **kwargs – Forwarded to DuckDBConnector.download_spatial_data(). Accepts where_clause (optional SQL WHERE fragment) and crs (output CRS).

Returns:

GeoDataFrame with spatial data.

Raises:
Return type:

None

siege_utilities.geo.spatial_transformations.execute_duckdb_query(query, db_path=None, **kwargs)[source]

Execute a spatial SQL query on DuckDB (optional).

Parameters:
  • query (str) – SQL query string.

  • db_path (str | None) – Path to DuckDB database file (uses user config or in-memory when omitted).

  • **kwargs – Forwarded to DuckDBConnector.execute_spatial_query(). Accepts crs (output CRS, applies to SELECT queries only).

Returns:

GeoDataFrame for SELECT queries, int rowcount for others.

Raises:
Return type:

None | int

Spatial runtime capability planning across Databricks/Spark/Python contexts.

Includes GeometryPayload for Spark-safe geometry transport and codec helpers (encode_geometry, decode_geometry, payload_to_spark_row, spark_row_to_payload) that round-trip shapely geometries through WKT / WKB / GeoJSON representations.

class siege_utilities.geo.spatial_runtime.SpatialRuntimePlan[source]

Bases: object

Execution plan for spatial operations with deterministic fallback order.

runtime: str
native_spatial_available: bool
sedona_available: bool
loader_order: List[str]
reason: str
__init__(runtime, native_spatial_available, sedona_available, loader_order, reason)
Parameters:
  • runtime (str)

  • native_spatial_available (bool)

  • sedona_available (bool)

  • loader_order (List[str])

  • reason (str)

Return type:

None

siege_utilities.geo.spatial_runtime.resolve_spatial_runtime_plan(*, databricks_runtime_version=None, native_spatial_available=None, sedona_available=None)[source]

Resolve spatial runtime execution plan.

Priority order: 1. databricks_native 2. sedona 3. python

Parameters:
  • databricks_runtime_version (str | None)

  • native_spatial_available (bool | None)

  • sedona_available (bool | None)

Return type:

SpatialRuntimePlan

class siege_utilities.geo.spatial_runtime.GeometryPayload[source]

Bases: object

Spark-safe container for a single geometry with optional multi-format encoding.

At least one of geometry_wkt, geometry_wkb, or geometry_geojson should be populated for the payload to be useful.

geometry_wkt: str | None = None
geometry_wkb: bytes | None = None
geometry_geojson: Dict[str, Any] | None = None
crs: str = 'EPSG:4326'
geometry_type: str | None = None
__init__(geometry_wkt=None, geometry_wkb=None, geometry_geojson=None, crs='EPSG:4326', geometry_type=None)
Parameters:
  • geometry_wkt (str | None)

  • geometry_wkb (bytes | None)

  • geometry_geojson (Dict[str, Any] | None)

  • crs (str)

  • geometry_type (str | None)

Return type:

None

siege_utilities.geo.spatial_runtime.encode_geometry(geom, fmt='wkt', crs='EPSG:4326')[source]

Encode a shapely geometry into a GeometryPayload.

Parameters:
  • geom (Any) – A shapely.geometry.base.BaseGeometry instance.

  • fmt (str) – Primary serialisation format – "wkt", "wkb", or "geojson". All three representations are always populated; fmt validates that the caller’s intended primary format is supported.

  • crs (str) – Coordinate reference system identifier stored on the payload.

Return type:

GeometryPayload

Raises:

ValueError – If fmt is not one of "wkt", "wkb", "geojson".

siege_utilities.geo.spatial_runtime.decode_geometry(payload)[source]

Decode a GeometryPayload back to a shapely geometry.

Tries WKT first, then WKB, then GeoJSON.

Return type:

shapely.geometry.base.BaseGeometry

Parameters:

payload (GeometryPayload)

siege_utilities.geo.spatial_runtime.payload_to_spark_row(payload)[source]

Convert a GeometryPayload to a Spark-safe dict.

Binary geometry_wkb is base64-encoded to a string; geometry_geojson is JSON-serialised. All resulting values are plain strings (or None).

Parameters:

payload (GeometryPayload)

Return type:

Dict[str, Any]

siege_utilities.geo.spatial_runtime.spark_row_to_payload(row)[source]

Reconstruct a GeometryPayload from a Spark-row dict.

Reverses the encoding performed by payload_to_spark_row().

Parameters:

row (Dict[str, Any])

Return type:

GeometryPayload

Configurable default CRS plus geometry-level CRS detection and axis-order-aware reprojection.

The default-CRS layer (get_default_crs / set_default_crs / reproject_if_needed) is the existing thin GeoDataFrame-shaped API that every spatial-returning function in siege_utilities.geo uses as its default. Call set_default_crs() once at the top of a notebook or script to change the default globally:

import siege_utilities as su
su.set_default_crs("EPSG:3857")

# All subsequent calls return Web Mercator unless overridden:
gdf = su.download_data(2020, "county")  # already in EPSG:3857

detect_crs() and reproject_geom() extend the module to geometry-level work with explicit axis-order control. Pattern lifted from a third-party tile-server audit (common/repos/ImportServerRepo.py:150-336, specifically the SRS handling at lines 255-274). The salvage source used the GDAL/OGR Python bindings (osgeo.osr) with OAMS_TRADITIONAL_GIS_ORDER to force lon-first axis order; SU does not declare osgeo as a dep, so this module is built on pyproj (which IS declared). pyproj.Transformer.from_crs(..., always_xy=True) is the documented modern equivalent of the OGR axis-mapping strategy.

Why the axis-order toggle exists: GeoJSON / Shapefile / KML files store coordinates lon-first (x, y) but the EPSG authority specifies lat-first (y, x) for many CRSes including EPSG:4326. The default "trad_gis" mode honors the file conventions (what most consumers expect). The "auth_compliant" mode honors the EPSG authority (what GIS standards-compliant code paths require). Most consumer code expects trad_gis; choosing auth_compliant without knowing why is a subtle source of transposed lat/lon bugs.

siege_utilities.geo.crs.get_default_crs()[source]

Return the current default CRS string (e.g. "EPSG:4326").

Return type:

str

siege_utilities.geo.crs.set_default_crs(crs)[source]

Set the default CRS used by all spatial-returning functions.

Parameters:

crs (str) – Any CRS string accepted by pyproj (e.g. "EPSG:4326", "EPSG:3857", "ESRI:102003").

Raises:

pyproj.exceptions.CRSError – If crs is not recognized by pyproj.

Return type:

None

siege_utilities.geo.crs.reproject_if_needed(gdf, crs=None)[source]

Reproject gdf to crs if it differs from the GeoDataFrame’s current CRS.

If crs is None, uses get_default_crs(). Returns the GeoDataFrame unchanged if it is already in crs or is empty.

When the GeoDataFrame has no CRS (gdf.crs is None) and a target is specified, the target CRS is set (not reprojected) on the assumption that the data is already in the target CRS. A warning is logged because this assumption may be wrong.

Parameters:

crs (str | None)

siege_utilities.geo.crs.detect_crs(geom_or_ds)[source]

Resolve the source CRS of a spatial object.

Parameters:

geom_or_ds (Any) –

One of:

  • A GeoDataFrame — reads .crs and returns an EPSG:<n> string when authoritative, otherwise the WKT.

  • A GeoJSON-like dict — reads ["crs"]["properties"]["name"] if present.

  • A fiona Collection — reads .crs / .crs_wkt.

  • A bare shapely geometry — bare geometries carry no CRS metadata; this returns None (callers should treat this as “consult get_default_crs()”).

  • None — returns None.

Returns:

An EPSG string ("EPSG:4326"), a WKT string, or None.

Return type:

str | None

Mirrors the SRS-detection branch of the salvage source (ImportServerRepo.py:250-253), which read the spatial reference from an OGR DataSource via GetGeometryRef().GetSpatialReference(). This implementation reads .crs / .crs_wkt from GeoDataFrames or fiona collections — same intent, declared-deps surface.

siege_utilities.geo.crs.distance_to_decimal_degrees(distance_meters, latitude)[source]

Convert a distance in meters to approximate decimal degrees.

Uses the WGS-84 ellipsoid approximation. The result is the longitudinal span at the given latitude (degrees of longitude shrink toward the poles).

Parameters:
  • distance_meters (float) – Distance in meters.

  • latitude (float) – Latitude in decimal degrees where the conversion applies.

Returns:

Approximate equivalent distance in decimal degrees of longitude at latitude.

Raises:

ValueError – If latitude is outside [-90, 90].

Return type:

float

siege_utilities.geo.crs.reproject_geom(geom, src_crs, dst_crs=4326, axis_order='trad_gis')[source]

Reproject a shapely geometry with explicit axis-order control.

Parameters:
  • geom – A shapely geometry (Point, LineString, Polygon, etc.) or None.

  • src_crs (Any) – The geometry’s source CRS, in any form pyproj.CRS.from_user_input accepts ("EPSG:4326", an integer EPSG code, a WKT string, a pyproj.CRS object).

  • dst_crs (Any) – Target CRS, in any form pyproj.CRS.from_user_input accepts. Defaults to 4326 (WGS 84).

  • axis_order (str) –

    One of:

    • "trad_gis" (default) — pyproj always_xy=True. Coordinates are interpreted and emitted as (lon, lat), matching GeoJSON, Shapefile, KML, and most consumer expectations. This is the modern-pyproj equivalent of osgeo.osr.OAMS_TRADITIONAL_GIS_ORDER used in the salvage source (ImportServerRepo.py:267-268).

    • "auth_compliant"always_xy=False. Coordinates follow the CRS authority’s declared axis order (lat-first for EPSG:4326). Use only if you know your input data is lat-first and your consumer expects lat-first.

Returns:

A new shapely geometry in the target CRS, or None if geom is None.

Raises:
  • ValueError – If axis_order is not one of the two accepted tokens, or if src_crs is None (a CRS-less geometry cannot be reprojected without an explicit source).

  • ImportError – If pyproj or shapely are not installed.

Round-trip note: 4326 -> 3857 -> 4326 on a Point should return the same coordinates within 1e-6 degrees. The test suite verifies this.

GEOID utilities for Census data processing.

This module provides functions for constructing, normalizing, and validating Census GEOIDs (Geographic Identifiers). GEOIDs are hierarchical codes that uniquely identify geographic areas.

GEOID Structure: - State: 2 digits (e.g., “06” for California) - County: 5 digits (state + county, e.g., “06037” for LA County) - Tract: 11 digits (state + county + tract, e.g., “06037101100”) - Block Group: 12 digits (state + county + tract + block group) - Block: 15 digits (state + county + tract + block)

Example usage:

from siege_utilities.geo import normalize_geoid, construct_geoid, validate_geoid, can_normalize_geoid

# Normalize a GEOID to proper length geoid = normalize_geoid(“6037”, “county”) # Returns “06037”

# Construct GEOID from components geoid = construct_geoid(state=”06”, county=”037”, geography=”county”)

# Validate GEOID format (strict by default — requires leading zeros) is_valid = validate_geoid(“06037”, “county”) # Returns True is_valid = validate_geoid(“6037”, “county”) # Returns False (missing leading zero)

# Check if a value can be normalized to a valid GEOID can_fix = can_normalize_geoid(“6037”, “county”) # Returns True

class siege_utilities.geo.geoid_utils.GEOIDValidator[source]

Bases: object

Callable validator for GEOID fields.

Validates that a GEOID is digits-only and the correct length for the given geography level.

Usage in Django models:

geoid = CharField(validators=[GEOIDValidator(‘tract’)])

__init__(geography_level)[source]
Parameters:

geography_level (str)

deconstruct()[source]
siege_utilities.geo.geoid_utils.normalize_geoid(geoid, geography_level)[source]

Normalize a GEOID to the standard format with proper zero-padding.

Parameters:
  • geoid (str | int) – GEOID value (can be string or int)

  • geography_level (str) – Geographic level (‘state’, ‘county’, ‘tract’, etc.)

Returns:

Normalized GEOID string with proper zero-padding

Raises:

ValueError – If geography level is unknown or GEOID is invalid

Return type:

str

Example

>>> normalize_geoid("6037", "county")
'06037'
>>> normalize_geoid(6, "state")
'06'
>>> normalize_geoid("6037101100", "tract")
'06037101100'
siege_utilities.geo.geoid_utils.normalize_geoid_column(df, geoid_column, geography_level, inplace=False)[source]

Normalize a GEOID column in a DataFrame.

Parameters:
  • df (pandas.DataFrame) – DataFrame containing GEOID column

  • geoid_column (str) – Name of the GEOID column

  • geography_level (str) – Geographic level for normalization

  • inplace (bool) – If True, modify DataFrame in place

Returns:

DataFrame with normalized GEOID column

Return type:

pandas.DataFrame

siege_utilities.geo.geoid_utils.construct_geoid(geography, state=None, county=None, tract=None, block_group=None, block=None, place=None)[source]

Construct a GEOID from component parts.

Parameters:
  • geography (str) – Target geography level

  • state (str | None) – State FIPS code (2 digits)

  • county (str | None) – County FIPS code (3 digits)

  • tract (str | None) – Census tract code (6 digits)

  • block_group (str | None) – Block group code (1 digit)

  • block (str | None) – Block code (4 digits)

  • place (str | None) – Place code (5 digits)

Returns:

Constructed GEOID string

Raises:

ValueError – If required components are missing

Return type:

str

Example

>>> construct_geoid("county", state="06", county="037")
'06037'
>>> construct_geoid("tract", state="06", county="037", tract="101100")
'06037101100'
siege_utilities.geo.geoid_utils.construct_geoid_from_row(row, geography_level)[source]

Construct a GEOID from a DataFrame row containing Census API response columns.

This function handles the column names returned by the Census API.

Parameters:
  • row (pandas.Series) – DataFrame row with Census columns (state, county, tract, etc.)

  • geography_level (str) – Target geography level

Returns:

Constructed GEOID string

Return type:

str

Example

# For a row from Census API response: # {‘state’: ‘06’, ‘county’: ‘037’, ‘tract’: ‘101100’, …} geoid = construct_geoid_from_row(row, ‘tract’) # Returns: ‘06037101100’

siege_utilities.geo.geoid_utils.parse_geoid(geoid, geography_level)[source]

Parse a GEOID into its component parts.

Parameters:
  • geoid (str) – GEOID string

  • geography_level (str) – Geography level of the GEOID

Returns:

Dictionary with component parts (state, county, tract, etc.)

Return type:

Dict[str, str]

Example

>>> parse_geoid("06037101100", "tract")
{'state': '06', 'county': '037', 'tract': '101100'}
siege_utilities.geo.geoid_utils.extract_parent_geoid(geoid, child_geography, parent_geography)[source]

Extract a parent GEOID from a child GEOID.

Supports extracting state, county, or tract as the parent level. Other parent levels raise ValueError.

Parameters:
  • geoid (str) – Child GEOID

  • child_geography (str) – Geography level of the input GEOID

  • parent_geography (str) – Target parent level ("state", "county", or "tract")

Returns:

Parent GEOID

Raises:

ValueError – If parent_geography is not one of the supported parent levels (state, county, tract).

Return type:

str

Example

>>> extract_parent_geoid("06037101100", "tract", "county")
'06037'
>>> extract_parent_geoid("060371011001", "block_group", "state")
'06'
siege_utilities.geo.geoid_utils.validate_geoid(geoid, geography_level, strict=True)[source]

Validate a GEOID format.

Census GEOIDs are always fixed-width, zero-padded strings. A value missing leading zeros (e.g., ‘6037’ instead of ‘06037’) is NOT a valid GEOID — it may be normalizable via normalize_geoid(), but it would fail joins against Census shapefiles.

Parameters:
  • geoid (str) – GEOID string to validate

  • geography_level (str) – Expected geography level

  • strict (bool) – If True (default), require exact length match. If False, allow shorter values that could be zero-padded.

Returns:

True if GEOID is valid, False otherwise

Return type:

bool

Example

>>> validate_geoid("06037", "county")
True
>>> validate_geoid("6037", "county")  # Missing leading zero
False
>>> validate_geoid("6037", "county", strict=False)
True
siege_utilities.geo.geoid_utils.can_normalize_geoid(geoid, geography_level)[source]

Check whether a value can be normalized to a valid GEOID.

Unlike validate_geoid(), this accepts values that are shorter than the expected length (e.g., ‘6037’ for county) because they can be zero-padded to a valid GEOID via normalize_geoid().

Parameters:
  • geoid (str | int) – Value to check (string or integer)

  • geography_level (str) – Expected geography level

Returns:

True if value can be normalized to a valid GEOID

Return type:

bool

Example

>>> can_normalize_geoid("6037", "county")
True
>>> can_normalize_geoid("06037", "county")
True
>>> can_normalize_geoid("CA037", "county")
False
>>> can_normalize_geoid("123456", "county")
False
siege_utilities.geo.geoid_utils.validate_geoid_column(df, geoid_column, geography_level, strict=True)[source]

Validate a GEOID column in a DataFrame.

Parameters:
  • df (pandas.DataFrame) – DataFrame containing GEOID column

  • geoid_column (str) – Name of the GEOID column

  • geography_level (str) – Expected geography level

  • strict (bool) – If True (default), require exact length match

Returns:

Boolean Series indicating validity of each GEOID

Return type:

pandas.Series

siege_utilities.geo.geoid_utils.prepare_geoid_for_join(df, geoid_column, geography_level, output_column=None)[source]

Prepare a GEOID column for joining with another dataset.

This function: 1. Normalizes GEOIDs to standard format 2. Validates GEOIDs 3. Optionally creates a new column for the normalized GEOID

Parameters:
  • df (pandas.DataFrame) – DataFrame with GEOID column

  • geoid_column (str) – Name of the GEOID column

  • geography_level (str) – Geography level

  • output_column (str | None) – Name for normalized column (defaults to geoid_column)

Returns:

DataFrame with normalized GEOID column

Return type:

pandas.DataFrame

siege_utilities.geo.geoid_utils.geoid_to_slug(geoid, geography_level)[source]

Convert a GEOID to a URL-friendly slug.

Encodes the state FIPS as a lowercase abbreviation and appends the remaining GEOID components separated by hyphens.

Parameters:
  • geoid (str) – Full GEOID string

  • geography_level (str) – Geography level of the GEOID

Returns:

URL-friendly slug string

Return type:

str

Example

>>> geoid_to_slug("06037101100", "tract")
'ca-037-tract-101100'
>>> geoid_to_slug("06", "state")
'ca'
>>> geoid_to_slug("0614", "cd")
'ca-cd-14'
siege_utilities.geo.geoid_utils.slug_to_geoid(slug)[source]

Convert a slug back to a GEOID.

Inverse of geoid_to_slug(). Parses the state abbreviation and component parts from the slug.

Parameters:

slug (str) – URL-friendly slug string

Returns:

Full GEOID string

Raises:

ValueError – If slug cannot be parsed

Return type:

str

Example

>>> slug_to_geoid("ca-037-tract-101100")
'06037101100'
>>> slug_to_geoid("ca")
'06'
siege_utilities.geo.geoid_utils.find_geoid_column(df)[source]

Find the GEOID column in a DataFrame.

Looks for common GEOID column names used in Census data.

Parameters:

df (pandas.DataFrame) – DataFrame to search

Returns:

Name of GEOID column if found, None otherwise

Return type:

str | None

exception siege_utilities.geo.geocoding.GeocodingError[source]

Bases: RuntimeError

Raised when Nominatim geocoding fails for reasons other than ‘no match’.

Covers network timeouts (after retries exhausted), service errors, parse errors, and other unexpected failures from the geocoder. Distinct from a legitimate “no result found” outcome, which still returns None. Use __cause__ to inspect the underlying exception.

Mirrors siege_utilities.geo.providers.census_geocoder.CensusGeocodeError.

siege_utilities.geo.geocoding.get_country_name(country_code)[source]

Get the full country name from a country code.

Parameters:

country_code – Two-letter country code (e.g., ‘us’, ‘gb’, ‘ca’)

Returns:

Full country name or the code if not found

Return type:

str

siege_utilities.geo.geocoding.get_country_code(country_name)[source]

Get the country code from a country name.

Parameters:

country_name – Full country name (e.g., ‘United States’, ‘Canada’)

Returns:

Two-letter country code, or None if not found.

Return type:

str | None

siege_utilities.geo.geocoding.list_countries()[source]

Get a list of all available countries with their codes.

Returns:

Dictionary mapping country codes to country names

Return type:

dict

siege_utilities.geo.geocoding.concatenate_addresses(street=None, city=None, state_province_area=None, postal_code=None, country=None)[source]

Concatenate address components into a single string suitable for geocoding. Returns a properly formatted address string.

siege_utilities.geo.geocoding.get_coordinates(query_address, country_codes=None, max_retries=3, server_url=None)[source]

Get coordinates (latitude, longitude) for an address using Nominatim.

Parameters:
  • query_address – The address to geocode

  • country_codes – Optional country code filter (defaults to US)

  • max_retries – Maximum number of retry attempts

  • server_url – Optional custom Nominatim server URL (e.g., “http://nominatim.nominatim.svc.cluster.local” for self-hosted). Defaults to None (public OSM Nominatim).

Returns:

(latitude, longitude), or None when Nominatim returned no match for the address (a legitimate non-error outcome).

Return type:

tuple

Raises:
  • ValueError – If query_address is empty or None.

  • GeocodingError – If geocoding failed due to a network, service, or parse error — i.e., the geocoder could not even attempt to match. Wraps the underlying exception via __cause__.

siege_utilities.geo.geocoding.use_nominatim_geocoder(query_address, id=None, country_codes=None, max_retries=3, server_url=None)[source]

Geocode an address using Nominatim with proper rate limiting and error handling. Returns the result as a JSON string for Spark UDF compatibility.

Parameters:
  • query_address – The address to geocode

  • id – An identifier for tracking

  • country_codes – Optional country code filter (defaults to US)

  • max_retries – Number of retry attempts for transient errors

  • server_url – Optional custom Nominatim server URL (e.g., “http://nominatim.nominatim.svc.cluster.local” for self-hosted). Defaults to None (public OSM Nominatim with rate limiting). When set, rate limiting is skipped (self-hosted servers don’t require it).

Returns:

JSON string of the geocoding result, or None when Nominatim returned no match for the address (a legitimate non-error outcome).

Raises:
  • ValueError – If query_address is empty or None.

  • GeocodingError – If geocoding failed due to a network, service, or parse error after retries (i.e., the geocoder could not even attempt to match). Wraps the underlying geopy exception via __cause__.

siege_utilities.geo.geocoding.geocode_with_nominatim_public(address, country_codes=None)[source]

Geocode a single address using the public Nominatim service.

Thin wrapper around get_coordinates() that forces the public endpoint (no custom server_url).

Parameters:
  • address – Address string to geocode.

  • country_codes – Optional country code filter (defaults to US).

Returns:

(lat, lon) tuple, or None if no match found.

Raises:
siege_utilities.geo.geocoding.geocode_addresses_with_nominatim(addresses, country_codes=None, max_retries=3, server_url=None)[source]

Batch-geocode a list of addresses using Nominatim.

Calls get_coordinates() for each address sequentially with Nominatim’s built-in rate limiting (1 s for public, 50 ms for self-hosted).

Parameters:
  • addresses – Iterable of address strings.

  • country_codes – Optional country code filter (defaults to US).

  • max_retries – Retry attempts per address.

  • server_url – Optional custom Nominatim server URL.

Returns:

List of {"address": str, "lat": float, "lon": float} dicts. Addresses that returned no match have lat and lon set to None. Addresses that raised GeocodingError are logged and included with None coordinates.

class siege_utilities.geo.geocoding.NominatimGeoClassifier[source]

Bases: object

A classifier for geocoding results using Nominatim. Provides methods to categorize and analyze geocoding results.

__init__()[source]
get_place_ranks_by_label(label)[source]

Reverse lookup on place_rank_dict: label → matching rank IDs.

Nominatim returns an integer place_rank per result (0=country … 10=building). This helper goes the other direction so callers can write filter(rank=cls.get_place_ranks_by_label("City")) without hard-coding the integer.

Returns [] (not None) when the label is unknown so the caller can in / iterate without a None-check.

get_importance_threshold_by_label(label)[source]

Reverse lookup on importance_dict: label → importance threshold.

Nominatim’s importance field is a float in [0, 1] that mixes Wikipedia hit-count, place type, and population. We bucket those floats into ordinal labels (Country / State / …) and this method returns the float threshold that corresponds to a label — useful when filtering a Nominatim result set programmatically.

Returns None when no label matches; callers should treat that as “don’t filter on importance.”

to_json()[source]

Serialize the classifier’s lookup tables to a JSON string.

Used to persist customised rank/importance overrides between runs or to ship a classifier configuration to a worker process. The result round-trips through from_json().

from_json(json_string)[source]

Replace the classifier’s lookup tables from a JSON string.

Parameters:

json_string – JSON produced by to_json().

Inverse of to_json() — overwrites place_rank_dict and importance_dict in place. Unknown top-level keys are ignored; missing keys leave the corresponding table empty rather than raising, so a partial payload doesn’t poison the classifier.

siege_utilities.geo.geocoding.validate_geocode_data_pandas(df, lat_col, lon_col, crs=None)[source]

Filter rows with invalid geographic coordinates.

Pandas equivalent of spark_utils.validate_geocode_data().

Parameters:
  • df (pandas.DataFrame) – Input DataFrame.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • crs – Optional CRS (pyproj CRS, EPSG int, or string). When supplied, the valid coordinate bounds are derived from the CRS area of use. Defaults to WGS84 (-90..90, -180..180).

Returns:

DataFrame with only rows whose coordinates fall within the valid range.

Return type:

pandas.DataFrame

siege_utilities.geo.geocoding.mark_valid_geocode_data_pandas(df, lat_col, lon_col, output_col='is_valid', crs=None)[source]

Add a boolean validity column without removing rows.

Pandas equivalent of spark_utils.mark_valid_geocode_data().

Parameters:
  • df (pandas.DataFrame) – Input DataFrame.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • output_col (str) – Name of the boolean column to add.

  • crs – Optional CRS (pyproj CRS, EPSG int, or string). When supplied, the valid coordinate bounds are derived from the CRS area of use. Defaults to WGS84 (-90..90, -180..180).

Returns:

Copy of df with output_col appended.

Return type:

pandas.DataFrame

class siege_utilities.geo.geocoding.SpatiaLiteCache[source]

Bases: object

SpatiaLite-backed local cache for geocoding results and boundary lookups.

Stores: - geocode results: address_hash → (lat, lon, WKT point, source, timestamp) - boundary lookups: geoid → (point WKT, boundary WKT, vintage_year) - crosswalk mappings: (source_geoid, target_geoid) → weight

Spatial index on point geometry enables bounding-box queries. The database file is portable across machines.

Usage:

cache = SpatiaLiteCache()
cache.put_geocode("123 Main St, Springfield, IL 62701", 39.7817, -89.6501, source="nominatim")
result = cache.get_geocode("123 Main St, Springfield, IL 62701")
__init__(db_path=None)[source]
Parameters:

db_path (str | None)

put_geocode(address, latitude, longitude, source='nominatim', raw_response=None)[source]

Store a geocoding result. Returns the address hash.

Parameters:
Return type:

str

get_geocode(address)[source]

Look up a cached geocoding result by address string.

Parameters:

address (str)

Return type:

Dict | None

get_geocode_or_fetch(address, country_codes=None, server_url=None)[source]

Look up cache, falling back to Nominatim if not cached.

Returns None when the cache misses AND Nominatim has no match for the address. Raises GeocodingError when Nominatim fails (network/service/parse) — callers that want fail-open behavior must catch it explicitly. Raises ValueError for an empty/None address.

Parameters:
  • address (str)

  • country_codes (str | None)

  • server_url (str | None)

Return type:

Dict | None

get_geocodes_in_bbox(lat_min, lat_max, lon_min, lon_max)[source]

Return all cached geocodes within a bounding box.

Parameters:
Return type:

List[Dict]

put_boundary(geoid, vintage_year, point_wkt=None, boundary_wkt=None, source='tiger')[source]

Cache a boundary lookup result.

Parameters:
  • geoid (str)

  • vintage_year (int)

  • point_wkt (str | None)

  • boundary_wkt (str | None)

  • source (str)

get_boundary(geoid, vintage_year)[source]

Look up a cached boundary by GEOID and vintage year.

Parameters:
  • geoid (str)

  • vintage_year (int)

Return type:

Dict | None

put_crosswalk(source_geoid, target_geoid, weight=1.0, source='census')[source]

Cache a crosswalk mapping.

Parameters:
get_crosswalk(source_geoid)[source]

Get all crosswalk targets for a source GEOID.

Parameters:

source_geoid (str)

Return type:

List[Dict]

stats()[source]

Return row counts for all cache tables.

Return type:

Dict[str, int]

clear(table=None)[source]

Clear cache tables. If table is None, clear all.

Parameters:

table (str | None)

close()[source]

Close the database connection.

Grid-agnostic dispatch wrappers over H3 and S2.

Lets consumer code call index_points() / index_polygon() without committing to one grid system at the call site. The wrappers infer the grid from which keyword arguments are present, with explicit override via grid="h3" or grid="s2".

Inference rules (applied in order):

  1. If grid= is passed → that wins.

  2. Else if any S2-only kwarg is present (max_cells, min_level, max_level, level) → S2.

  3. Else if resolution= is present → H3.

  4. Else → H3 (the more common viz path).

Mixing resolution= with S2-only kwargs in one call raises ValueError rather than guessing — the caller should pass grid= explicitly to disambiguate.

These wrappers exist to keep reporting/ and consumer modules backend-agnostic; they don’t add functionality beyond what the underlying h3_utils / s2_utils modules already do.

siege_utilities.geo.grids.Grid

Recognised grid= argument values. None means “infer”.

alias of str | None

siege_utilities.geo.grids.index_points(df, lat_col, lon_col, *, grid=None, resolution=None, level=None, engine=None, **kwargs)[source]

Compute a grid cell ID for each row in df.

Dispatches to h3_index_points() or s2_index_points() based on the inference rules. Pass resolution= for H3 (default 8) or level= for S2 (default 12). Extra keyword arguments are forwarded to the underlying function (e.g. as_token=False for S2).

Returns:

pd.Series of cell IDs (H3 hex strings or S2 cell tokens by default).

Parameters:
  • df (Any)

  • lat_col (str)

  • lon_col (str)

  • grid (str | None)

  • resolution (int | None)

  • level (int | None)

  • engine (Any)

  • kwargs (Any)

Return type:

Any

siege_utilities.geo.grids.index_polygon(geometry, *, grid=None, resolution=None, level=None, max_cells=None, min_level=None, max_level=None, refine=True)[source]

Cover a polygon with grid cells.

With H3: returns a set of hex IDs at the requested resolution (uniform).

With S2: if max_cells is provided (or any of min_level, max_level), returns a list of int64 cell IDs from s2_region_cover() (variable-resolution coverage with a cell- count budget). Otherwise returns a set of int64 cell IDs at a single level from s2_index_polygon().

The shape of the return value (set vs list) intentionally signals which mode you got. For SQL-friendly cell-id ranges, see s2_cells_to_ranges().

Parameters:
  • grid (str | None)

  • resolution (int | None)

  • level (int | None)

  • max_cells (int | None)

  • min_level (int | None)

  • max_level (int | None)

  • refine (bool)

Return type:

set | list

siege_utilities.geo.grids.infer_grid(grid, kwargs)[source]

Resolve grid= per the documented inference rules.

Returns "h3" or "s2"; raises ValueError if the inputs contradict the chosen grid. Pure function — does not mutate kwargs.

Parameters:
Return type:

str

H3 hexagonal hierarchical spatial index utilities.

Provides lightweight spatial join approximation using Uber’s H3 indexing system. This serves as a “geo-lite” fallback when full GeoPandas point-in-polygon operations are too expensive or when an approximate spatial join is acceptable.

Requires: h3>=4.0 (primary), with v3 fallback support.

siege_utilities.geo.h3_utils.h3_hex_to_boundary(hex_id)[source]

Return the boundary coordinates for an H3 hex cell.

Parameters:

hex_id (str) – H3 hex ID string.

Returns:

list of (lat, lng) tuples forming the hex boundary (closed ring).

Raises:

ImportError – If h3 is not installed.

Return type:

list

siege_utilities.geo.h3_utils.h3_index_points(df, lat_col, lon_col, resolution=8)[source]

Compute H3 hex index for each point in a DataFrame.

Parameters:
  • df (pandas.DataFrame) – DataFrame with latitude and longitude columns.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • resolution (int) – H3 resolution (0-15). Default 8 (~0.74 km^2 hexes).

Returns:

pd.Series of H3 hex ID strings, indexed like the input DataFrame.

Raises:
  • ImportError – If h3 is not installed.

  • ValueError – If resolution is out of range or columns are missing.

Return type:

pandas.Series

siege_utilities.geo.h3_utils.h3_index_polygon(geometry, resolution=8)[source]

Return the set of H3 hexes covering a polygon geometry.

Accepts a Shapely Polygon/MultiPolygon or a GeoJSON-like dict.

Parameters:
  • geometry – A Shapely Polygon/MultiPolygon, or a GeoJSON-like dict with type and coordinates keys.

  • resolution (int) – H3 resolution (0-15). Default 8.

Returns:

set of H3 hex ID strings that cover the polygon.

Raises:
Return type:

set

siege_utilities.geo.h3_utils.h3_resolution_for_admin_level(level)[source]

Suggest an H3 resolution whose average hex area matches a US admin level.

Useful when you want to bin point data into hexes that are roughly the same scale as a familiar admin unit (e.g. “give me hexes the size of counties”). The mapping is deliberately approximate — admin polygons vary by orders of magnitude within a single level, so this is a starting point, not a precision tool.

Recognised levels (case-insensitive):

state, county, zcta, tract, block_group, block.

Common aliases are accepted (zipzcta, bgblock_group, plurals, etc.).

Parameters:

level (str) – Admin geography level name.

Returns:

H3 resolution (0-15) closest in average hex area to the average polygon area at the requested admin level.

Return type:

int

Raises:
siege_utilities.geo.h3_utils.h3_resolution_for_area(target_area_km2)[source]

Suggest the H3 resolution whose hex area best matches a target area.

Parameters:

target_area_km2 (float) – Target hex area in square kilometres.

Returns:

H3 resolution (0-15) whose average hex area is closest to the target. Comparison is done in log-space for better matching across orders of magnitude.

Return type:

int

Raises:
siege_utilities.geo.h3_utils.h3_spatial_join(points_df, polygons_gdf, lat_col, lon_col, resolution=8, polygon_id_col=None)[source]

Join points to polygons via H3 hex matching (approximate point-in-polygon).

This is a lightweight alternative to GeoPandas sjoin. Each polygon is decomposed into H3 hexes, and each point is assigned to the hex it falls in. Points and polygons sharing a hex are joined.

Note: This is an approximation. Points near polygon boundaries may be misclassified due to hex granularity.

Parameters:
  • points_df (pandas.DataFrame) – DataFrame with latitude/longitude columns.

  • polygons_gdf – GeoDataFrame (or any object with a geometry column and an iterable of rows). Each geometry must be a Polygon or MultiPolygon.

  • lat_col (str) – Name of the latitude column in points_df.

  • lon_col (str) – Name of the longitude column in points_df.

  • resolution (int) – H3 resolution (0-15). Default 8.

  • polygon_id_col (str | None) – Optional column in polygons_gdf to use as the polygon identifier. If None, the index is used.

Returns:

pd.DataFrame with the original point columns plus polygon attributes for matched rows. Unmatched points are excluded.

Raises:

ImportError – If h3 is not installed.

Return type:

pandas.DataFrame

S2 cell-grid utilities.

Sibling of siege_utilities.geo.h3_utils. Where H3 gives you uniform hexagonal cells at a single resolution, S2 gives you square cells with integer IDs that sort by spatial proximity. The same lat/lon-to-cell operations are mirrored across both modules; the S2-specific functions exposed here (cell-id ↔ uint64, bbox-to-cells, parent/children, region cover with cell-count budget, range-bound generation for SQL) are the reason to reach for S2 rather than H3.

Why both

Use case

Reach for

Why

Density choropleth, hex-bins

H3

Uniform cell area

K-nearest-neighbors via k-ring

H3

Uniform neighbor distances

Spatial primary key in DB

S2

Sortable int64 IDs

Bbox / range queries in SQL

S2

Cell-id ranges = bboxes

Hierarchical rollups

S2

4 children per cell, clean

Apple Photos / iOS data

S2

Native to that ecosystem

Polygon coverage limitation

The pure-Python s2sphere package supports LatLngRect (bounding boxes) but not the full Loop / Polygon types from C++ S2. s2_index_polygon() covers the polygon’s bounding box and optionally post-filters cells against the actual geometry using shapely. For a heavily concave polygon (e.g. Idaho), the bbox cover is loose; the post-filter step (default on) trims cells whose centers fall outside.

Requires: s2sphere>=0.2.5.

siege_utilities.geo.s2_utils.s2_index_points(df, lat_col, lon_col, level=12, *, as_token=True)[source]

Compute the S2 cell ID for each point.

Parameters:
  • df (pd.DataFrame) – DataFrame with latitude / longitude columns.

  • lat_col (str) – Name of the latitude column.

  • lon_col (str) – Name of the longitude column.

  • level (int) – S2 level (0–30). Default 12 (~5.8 km² per cell).

  • as_token (bool) – If True (default), return the cell as a 16-char hex token string (compact, human-readable, JSON-safe). If False, return the int64 cell ID (sortable, ideal for database keys). Both round-trip via the s2_cell_id_to_token() / s2_token_to_cell_id() helpers.

Returns:

"s2_index".

Return type:

pd.Series indexed like the input. Name

Raises:
  • ImportError – If s2sphere is not installed.

  • ValueError – If level is out of range or columns are missing.

siege_utilities.geo.s2_utils.s2_index_polygon(geometry, level=12, *, refine=True)[source]

Return the set of S2 cells at level covering geometry.

Single-level coverage — symmetric with h3_index_polygon(). For variable-resolution coverage with a cell-count budget, see s2_region_cover().

Because the pure-Python s2sphere lacks polygon support, this function covers the polygon’s bounding box and (by default) post- filters the result to cells whose centers fall inside the actual geometry, using shapely. Set refine=False to skip the filter when the input is a near-rectangle (e.g. a county) and you want the bbox cover unchanged.

Parameters:
  • geometry – A shapely Polygon / MultiPolygon, or a GeoJSON-like dict.

  • level (int) – S2 level (0–30). All returned cells are at this level.

  • refine (bool) – Filter cells by checking whether the cell center lies inside the polygon (default True).

Returns:

set of int64 cell IDs.

Raises:
  • ImportError – If s2sphere (or shapely, when refine=True) is missing.

  • ValueError – If level is out of range.

  • TypeError – If geometry can’t be converted to a shapely shape.

Return type:

set

siege_utilities.geo.s2_utils.s2_region_cover(geometry, *, min_level=0, max_level=30, max_cells=100)[source]

Cover geometry’s bounding box with at most max_cells S2 cells.

The S2-specific operation: returns a mixed-resolution set of cells that together cover the bounding box, preferring coarse cells in the interior and fine cells along boundaries. The output is what you want for a database-side spatial-index lookup.

Use s2_cells_to_ranges() to turn the result into (min, max) int64 ranges suitable for a SQL BETWEEN clause.

Parameters:
  • geometry – shapely shape or GeoJSON dict.

  • min_level (int) – Minimum S2 level the coverer may use.

  • max_level (int) – Maximum S2 level the coverer may use.

  • max_cells (int) – Cell-count budget. The coverer returns at most this many cells (often fewer).

Returns:

A list of int64 cell IDs at varying levels, ordered as the coverer produced them (sortable but not sorted).

Return type:

list[int]

siege_utilities.geo.s2_utils.s2_spatial_join(points_df, polygons_gdf, lat_col, lon_col, level=12, polygon_id_col=None)[source]

Join points to polygons via S2 cell matching (approximate PiP).

Same shape as h3_spatial_join(). Points and polygons sharing a cell are joined; polygons are decomposed into S2 cells via s2_index_polygon(). First-polygon-wins on overlap.

Parameters:
  • points_df (pd.DataFrame)

  • lat_col (str)

  • lon_col (str)

  • level (int)

  • polygon_id_col (Optional[str])

Return type:

pd.DataFrame

siege_utilities.geo.s2_utils.s2_cell_to_boundary(cell_id)[source]

Return the 4 vertex coordinates of a cell as (lat, lng) tuples.

Accepts an int64 cell ID or a hex token string.

Return type:

list[tuple[float, float]]

siege_utilities.geo.s2_utils.s2_level_for_area(target_area_km2)[source]

Suggest the S2 level whose average cell area matches target_area_km2.

Comparison is in log-space, mirroring h3_resolution_for_area().

Parameters:

target_area_km2 (float)

Return type:

int

siege_utilities.geo.s2_utils.s2_level_for_admin_level(level)[source]

Suggest an S2 level whose average cell area matches a US admin level.

Same admin levels recognised by h3_resolution_for_admin_level() (state / county / zcta / tract / block_group / block, with the usual aliases). Returns the S2 level that’s closest in log-space.

Parameters:

level (str)

Return type:

int

siege_utilities.geo.s2_utils.s2_kring(cell_id, k=1)[source]

Return cell IDs within k steps of cell_id on the same level.

The k-ring is approximate for S2 (squares don’t tile uniformly); we walk neighbors recursively. For exact k-ring on a uniform grid use h3_kring() instead.

Parameters:

k (int)

Return type:

list[int]

siege_utilities.geo.s2_utils.s2_distance(a, b)[source]

Edge-step distance between two S2 cells at the same level.

Counts cell-boundary crossings via repeated neighbor expansion until b is reached. O(distance); use only for small radii. For large distances, switch to great-circle distance on the cell centers.

Return type:

int

siege_utilities.geo.s2_utils.s2_parent(cell_id, level)[source]

Return the ancestor cell at level (must be ≤ current level).

Parameters:

level (int)

Return type:

int

siege_utilities.geo.s2_utils.s2_children(cell_id)[source]

Return the four immediate children of a cell.

Raises if called on a leaf cell (level 30).

Return type:

list[int]

siege_utilities.geo.s2_utils.s2_cell_id_to_uint64(cell_or_token)[source]

Return the int64 form of a cell, accepting either a token or an id.

Useful when receiving a token string (e.g. "88891b") and needing the integer for a database column. Accepts any integer-like type (Python int, numpy.int64, numpy.uint64) for round-trip convenience.

Return type:

int

siege_utilities.geo.s2_utils.s2_uint64_to_cell_id(n)[source]

Return an s2sphere.CellId from an int64 value.

Parameters:

n (int)

siege_utilities.geo.s2_utils.s2_cell_id_to_token(cell_id)[source]

Return the compact hex token form of a cell.

Tokens are stable, JSON-safe, and 16 characters or fewer.

Return type:

str

siege_utilities.geo.s2_utils.s2_token_to_cell_id(token)[source]

Parse a hex token back to its int64 cell ID.

Parameters:

token (str)

Return type:

int

siege_utilities.geo.s2_utils.s2_bbox_to_cells(min_lat, min_lon, max_lat, max_lon, *, max_level=18, max_cells=64)[source]

Cover an axis-aligned bounding box with at most max_cells cells.

Convenience wrapper around s2_region_cover() for the common case of a map-viewport query. Returns int64 cell IDs at varying levels.

Parameters:
Return type:

list[int]

siege_utilities.geo.s2_utils.s2_cells_to_ranges(cell_ids)[source]

Convert cell IDs to (range_min, range_max) pairs for SQL.

Each S2 cell, regardless of level, has a contiguous range of leaf cell IDs that lie within it: [cell.range_min, cell.range_max]. Pre-computing those ranges lets a SQL query test “is point’s cell inside this region” with a simple WHERE leaf_cell_id BETWEEN min AND max (one comparison per cell in the cover, no spatial library needed at query time).

Parameters:

cell_ids (Iterable[int])

Return type:

list[tuple[int, int]]

Isochrone utilities with open-source-first providers and custom server support.

This module provides both a function-based API (backward-compatible) and a provider-based OOP architecture via IsochroneProvider and its concrete subclasses OpenRouteServiceProvider and ValhallaProvider.

Use get_provider() to obtain a configured provider instance by name.

exception siege_utilities.geo.isochrones.IsochroneError[source]

Bases: Exception

Base exception for isochrone operations.

exception siege_utilities.geo.isochrones.IsochroneNetworkError[source]

Bases: IsochroneError

Network failure (timeout, connection refused, DNS resolution).

class siege_utilities.geo.isochrones.IsochroneProvider[source]

Bases: ABC

Abstract base class for isochrone providers.

Subclasses must implement fetch(), validate_config(), and the provider_name property.

abstractmethod fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

abstractmethod validate_config()[source]

Check whether this provider is properly configured.

Returns:

True if the provider configuration is valid, False otherwise.

Return type:

bool

abstract property provider_name: str

Return the canonical name of this provider.

exception siege_utilities.geo.isochrones.IsochroneProviderError[source]

Bases: IsochroneError

Provider returned an HTTP error or an unparseable response.

class siege_utilities.geo.isochrones.IsochroneRequest[source]

Bases: TypedDict

Structured request definition returned by build_isochrone_request().

provider: Literal['openrouteservice', 'valhalla']
method: Literal['POST', 'GET']
url: str
headers: Dict[str, str]
params: Dict[str, Any]
json: Dict[str, Any]
class siege_utilities.geo.isochrones.OpenRouteServiceProvider[source]

Bases: IsochroneProvider

Concrete IsochroneProvider wrapping the OpenRouteService API.

Parameters:
  • api_key – ORS API key. Required for the hosted service; optional for self-hosted instances.

  • base_url – Root URL of the ORS server. Defaults to DEFAULT_ORS_BASE_URL.

  • timeout_seconds – HTTP timeout in seconds (default 30).

  • max_retries – Maximum retry attempts for transient failures (default 3).

__init__(api_key=None, base_url=None, timeout_seconds=30, max_retries=3)[source]
Parameters:
  • api_key (str | None)

  • base_url (str | None)

  • timeout_seconds (int)

  • max_retries (int)

Return type:

None

property provider_name: str

Return the canonical name of this provider.

validate_config()[source]

Check that the provider is configured.

For the hosted ORS service (default URL), an API key is required. For self-hosted instances, the base URL must be non-empty.

Return type:

bool

fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

class siege_utilities.geo.isochrones.ValhallaProvider[source]

Bases: IsochroneProvider

Concrete IsochroneProvider wrapping a Valhalla server.

Parameters:
  • base_url – Root URL of the Valhalla server. Defaults to DEFAULT_VALHALLA_BASE_URL.

  • timeout_seconds – HTTP timeout in seconds (default 30).

  • max_retries – Maximum retry attempts for transient failures (default 3).

__init__(base_url=None, timeout_seconds=30, max_retries=3)[source]
Parameters:
  • base_url (str | None)

  • timeout_seconds (int)

  • max_retries (int)

Return type:

None

property provider_name: str

Return the canonical name of this provider.

validate_config()[source]

Check that the base URL is configured.

Return type:

bool

fetch(lat, lon, time_minutes, profile='driving-car')[source]

Fetch an isochrone as a GeoJSON dict.

Parameters:
  • lat (float) – Center latitude (-90 to 90).

  • lon (float) – Center longitude (-180 to 180).

  • time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • profile (str) – Travel profile (e.g. "driving-car").

Returns:

A GeoJSON dict (typically a FeatureCollection).

Return type:

Dict[str, Any]

siege_utilities.geo.isochrones.build_isochrone_request(latitude, longitude, travel_time_minutes, *, provider='openrouteservice', base_url=None, profile='driving-car', api_key=None, extra_params=None)[source]

Build a provider-specific HTTP request definition for an isochrone call.

This supports open-source providers and self-hosted/custom endpoints.

Parameters:
  • latitude (float) – Center latitude (-90 to 90).

  • longitude (float) – Center longitude (-180 to 180).

  • travel_time_minutes (int) – Travel-time contour in minutes (must be > 0).

  • provider (str) – "openrouteservice" (or "ors") or "valhalla".

  • base_url (str | None) – Custom server root for self-hosted providers.

  • profile (str) – Travel profile ("driving-car", "cycling-regular", "foot-walking", etc.).

  • api_key (str | None) – Optional API key (commonly used for hosted ORS).

  • extra_params (Mapping[str, Any] | None) – Provider-specific extra request fields merged into the JSON body (Valhalla) or query params (ORS).

Returns:

An IsochroneRequest dict with provider, method, url, headers, params, and json keys.

Raises:

ValueError – If inputs are out of range or provider is unknown.

Return type:

IsochroneRequest

siege_utilities.geo.isochrones.get_isochrone(latitude, longitude, travel_time_minutes, *, provider='openrouteservice', base_url=None, profile='driving-car', api_key=None, timeout_seconds=30, extra_params=None, max_retries=3)[source]

Fetch an isochrone GeoJSON response.

Parameters:
  • latitude (float) – Center latitude.

  • longitude (float) – Center longitude.

  • travel_time_minutes (int) – Travel-time contour in minutes.

  • provider (str) – "openrouteservice" (default) or "valhalla".

  • base_url (str | None) – Custom server root for self-hosted providers.

  • profile (str) – Travel profile ("driving-car", "cycling-regular", "foot-walking", etc.).

  • api_key (str | None) – Optional API key (commonly used for hosted ORS).

  • timeout_seconds (int) – HTTP timeout in seconds (default 30).

  • extra_params (Mapping[str, Any] | None) – Provider-specific extra request fields.

  • max_retries (int) – Maximum number of attempts for transient failures (default 3). Set to 1 to disable retries.

Returns:

Provider response parsed as JSON (typically a GeoJSON FeatureCollection).

Raises:
Return type:

Dict[str, Any]

siege_utilities.geo.isochrones.get_provider(name='ors', **kwargs)[source]

Factory function returning a configured IsochroneProvider.

Parameters:
  • name (str) – Provider name — "ors" / "openrouteservice" or "valhalla".

  • **kwargs (Any) – Passed through to the provider constructor (e.g. api_key, base_url, timeout_seconds).

Returns:

A configured provider instance.

Raises:

ValueError – If name is not a recognised provider.

Return type:

IsochroneProvider

siege_utilities.geo.isochrones.isochrone_to_geodataframe(isochrone_geojson, *, crs=None)[source]

Convert an isochrone GeoJSON object to a GeoDataFrame.

Requires geopandas. Install with pip install siege_utilities[geo].

GeoJSON is always ingested as EPSG:4326 (the GeoJSON spec mandates WGS 84). If crs differs from EPSG:4326 the result is reprojected to the requested CRS before returning.

Parameters:
  • isochrone_geojson (Mapping[str, Any]) – A GeoJSON dict (typically a FeatureCollection returned by get_isochrone()).

  • crs (str | None) – Target coordinate reference system. Defaults to get_default_crs() (initially "EPSG:4326"). Pass any string accepted by pyproj.

Returns:

A GeoDataFrame with one row per feature in the requested crs.

Raises:

ImportError – If geopandas is not installed.

Return type:

gpd.GeoDataFrame

GEOID validators for Django models and standalone use.

Provides both Django field validators and standalone validation functions for Census GEOID formats at various geographic levels.

Example usage (standalone):

from siege_utilities.geo.validators import is_valid_state_fips, is_valid_tract_geoid

is_valid_state_fips(“06”) # True (California) is_valid_tract_geoid(“06037101100”) # True

Example usage (Django):

from siege_utilities.geo.validators import StateFIPSValidator

class MyModel(models.Model):

state_fips = models.CharField(max_length=2, validators=[StateFIPSValidator()])

class siege_utilities.geo.validators.BlockGroupGEOIDValidator[source]

Bases: object

Django validator for 12-digit block group GEOIDs.

message = 'Enter a valid 12-digit Census block group GEOID.'
code = 'invalid_block_group_geoid'
class siege_utilities.geo.validators.CountyFIPSValidator[source]

Bases: object

Django validator for 5-digit county FIPS codes.

message = 'Enter a valid 5-digit county FIPS code (2-digit state + 3-digit county).'
code = 'invalid_county_fips'
class siege_utilities.geo.validators.StateFIPSValidator[source]

Bases: object

Django validator for 2-digit state FIPS codes.

message = 'Enter a valid 2-digit state FIPS code.'
code = 'invalid_state_fips'
class siege_utilities.geo.validators.TractGEOIDValidator[source]

Bases: object

Django validator for 11-digit tract GEOIDs.

message = 'Enter a valid 11-digit Census tract GEOID.'
code = 'invalid_tract_geoid'
siege_utilities.geo.validators.is_valid_block_group_geoid(value)[source]

Validate a 12-digit block group GEOID (state + county + tract + block group).

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.validators.is_valid_county_fips(value)[source]

Validate a 5-digit county FIPS code (2-digit state + 3-digit county).

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.validators.is_valid_state_fips(value)[source]

Validate a 2-digit state FIPS code against known codes.

Parameters:

value (str)

Return type:

bool

siege_utilities.geo.validators.is_valid_tract_geoid(value)[source]

Validate an 11-digit tract GEOID (2-digit state + 3-digit county + 6-digit tract).

Parameters:

value (str)

Return type:

bool

NCES Urban-Centric Locale Classification for siege_utilities.

Implements the National Center for Education Statistics (NCES) 12-code locale classification system. Classifies arbitrary geographic points and polygons into City/Suburb/Town/Rural categories with size/distance subtypes.

Two modes of operation: 1. Pre-computed: Uses NCES-published locale boundary shapefiles (fastest) 2. On-the-fly: Computes from Census UA/UC/Place inputs (any year)

See docs/design/nces-locale-classification.md for full algorithm description.

class siege_utilities.geo.locale.LocaleCode[source]

Bases: object

An NCES 12-code locale classification result.

code

Integer locale code (11-43).

Type:

int

category

Major type — city, suburban, town, or rural.

Type:

str

subcategory

Full subtype — city_large, rural_remote, etc.

Type:

str

label

Human-readable label — City-Large, Rural-Remote, etc.

Type:

str

code: int
category: str
subcategory: str
label: str
classmethod from_code(code)[source]

Construct a LocaleCode from an integer code (11-43).

Raises:

ValueError – If the code is not a valid NCES locale code.

Parameters:

code (int)

Return type:

LocaleCode

__init__(code, category, subcategory, label)
Parameters:
Return type:

None

class siege_utilities.geo.locale.LocaleIndex[source]

Bases: object

Precomputed GEOID → LocaleCode mapping for fast lookup.

Built from any data source that pairs GEOIDs with NCES locale codes (e.g., NCES district data, school locations, or Census tract assignments).

__init__(mapping=None)[source]
Parameters:

mapping (Dict[str, int] | None)

add(geoid, locale_code)[source]
Parameters:
  • geoid (str)

  • locale_code (int)

Return type:

None

add_bulk(pairs)[source]
Parameters:

pairs (Dict[str, int])

Return type:

None

get(geoid)[source]
Parameters:

geoid (str)

Return type:

LocaleCode | None

classmethod from_dataframe(df, geoid_col='geoid', locale_col='locale_code')[source]
Parameters:
Return type:

LocaleIndex

class siege_utilities.geo.locale.LocaleType[source]

Bases: IntEnum

NCES major locale types.

CITY = 1
SUBURB = 2
TOWN = 3
RURAL = 4
__new__(value)
class siege_utilities.geo.locale.NCESLocaleClassifier[source]

Bases: object

Classify geographic points and polygons into NCES locale codes.

Uses Census TIGER spatial data (Urbanized Areas, Urban Clusters, Places) and population thresholds to implement the NCES urban-centric locale classification algorithm (12-code system).

The classifier can be initialized with pre-loaded GeoDataFrames (for testing or custom data) or constructed from Census downloads via the from_census_year and from_nces_boundaries factory methods.

Parameters:
  • urbanized_areas – GeoDataFrame of UA polygons (pop >= 50,000). Must contain a geometry column and a population field.

  • urban_clusters – GeoDataFrame of UC polygons (pop 2,500-49,999). Must contain a geometry column. May be None for 2020+ Census data where UCs were eliminated.

  • principal_cities – GeoDataFrame of Place polygons that are designated principal cities of CBSAs.

  • place_populations – Dict mapping place identifiers to population counts. Used to classify City subtypes (Large/Midsize/Small).

  • ua_populations – Optional dict mapping UA identifiers to populations. Used to classify Suburb subtypes. If None, reads from the urbanized_areas GeoDataFrame attributes.

  • projection_crs – EPSG code for distance computations. Defaults to settings.PROJECTION_CRS (2163, US National Atlas Equal Area). Override for Alaska (3338) or Hawaii (26963).

__init__(urbanized_areas, urban_clusters, principal_cities, place_populations, ua_populations=None, projection_crs=None)[source]
Parameters:
  • urbanized_areas (Any)

  • urban_clusters (Any | None)

  • principal_cities (Any)

  • place_populations (Dict[str, int])

  • ua_populations (Dict[str, int] | None)

  • projection_crs (int | None)

property locale_index: LocaleIndex | None
classify_geoid(geoid)[source]

Fast-path locale lookup by Census GEOID.

Returns the precomputed locale code if the GEOID is in the index, or None if no index is set or the GEOID is not found. Use classify_geoid_or_point() for automatic fallback to spatial classification.

Parameters:

geoid (str)

Return type:

LocaleCode | None

classify_geoid_or_point(geoid, lon, lat)[source]

Classify by GEOID if available, falling back to spatial.

Parameters:
Return type:

LocaleCode

classify_point(lon, lat)[source]

Classify a single geographic point.

Implements the NCES decision tree: 1. Spatial join against Urbanized Areas 2. If in UA: check principal city → City or Suburb 3. If not in UA: check Urban Cluster → Town (by distance to UA) 4. If not in UA/UC → Rural (by distance to UA and UC)

Parameters:
  • lon (float) – Longitude in the input CRS (default NAD83/WGS84).

  • lat (float) – Latitude in the input CRS (default NAD83/WGS84).

Returns:

A LocaleCode describing the point’s NCES classification.

Return type:

LocaleCode

classify_points(gdf, geometry_col='geometry')[source]

Bulk classify a GeoDataFrame of points.

Adds columns: locale_code (int), locale_category (str), locale_subcategory (str), locale_label (str).

Parameters:
  • gdf (Any) – GeoDataFrame with point geometries.

  • geometry_col (str) – Name of the geometry column.

Returns:

The input GeoDataFrame with locale columns added.

Return type:

Any

classify_polygon(polygon, method='area_weighted')[source]

Classify a polygon by locale distribution.

Parameters:
  • polygon (Any) – A Shapely polygon or multipolygon geometry.

  • method (str) – "area_weighted", "majority", or "distribution".

Returns:

{"locale_code": 21, "locale_label": "..."} If method="distribution" or "area_weighted": dict of {code: fraction} pairs summing to ~1.0.

Return type:

If method="majority"

Raises:

ValueError – If method is not one of the valid options.

classify_polygons(gdf, method='majority', geometry_col='geometry')[source]

Bulk classify a GeoDataFrame of polygons.

For method="majority", adds columns: locale_code (int), locale_label (str).

For method="distribution" or "area_weighted", adds a locale_distribution column containing per-row dicts of {code: fraction} pairs.

Parameters:
  • gdf (Any) – GeoDataFrame with polygon geometries.

  • method (str) – "majority", "distribution", or "area_weighted".

  • geometry_col (str) – Name of the geometry column.

Returns:

The input GeoDataFrame with locale columns added.

Raises:

ValueError – If method is not one of the valid options.

Return type:

Any

classmethod from_census_year(year=2020, cache_dir=None, projection_crs=None)[source]

Construct a classifier from Census TIGER downloads.

Downloads UA/UC (uac), Place, and CBSA shapefiles for the given year, identifies principal cities from population thresholds, and loads population estimates from shapefile attributes.

For 2020+ Census data where Urban Clusters were eliminated, this method derives a UC-equivalent set by filtering urban areas with population < 50,000 (the original UC threshold). This ensures Town codes (31-33) remain reachable.

Principal cities are identified by population threshold (>= 25,000), not from the OMB CBSA delineation file. This is a best-effort approximation. For precise NCES-matching classification, use from_nces_boundaries() when available.

Parameters:
  • year (int) – Census year for boundary data (2010 or 2020 recommended).

  • cache_dir (str | None) – Local directory for caching downloaded shapefiles.

  • projection_crs (int | None) – EPSG code for distance computation.

Returns:

An initialized NCESLocaleClassifier.

Raises:

ImportError – If geopandas is not available.

Return type:

NCESLocaleClassifier

classmethod from_nces_boundaries(year=2023, cache_dir=None, projection_crs=None)[source]

Construct a classifier from NCES pre-computed locale boundary shapefiles.

NCES publishes locale territory polygons that are already classified with locale codes (11-43). This is faster than computing from Census inputs but limited to years NCES has published.

The downloaded boundaries are split into: - Urbanized Area proxies: City (11-13) + Suburb (21-23) territories - Urban Cluster proxies: Town (31-33) territories - Principal city proxies: City (11-13) territories only

The classifier then works identically to the Census-based one.

Parameters:
  • year (int) – NCES publication year.

  • cache_dir (str | None) – Local directory for caching downloaded shapefiles.

  • projection_crs (int | None) – EPSG code for distance computation.

Returns:

An initialized NCESLocaleClassifier.

Return type:

NCESLocaleClassifier

static locale_label(code)[source]

Convert an integer locale code to a human-readable label.

>>> NCESLocaleClassifier.locale_label(11)
'City-Large'
>>> NCESLocaleClassifier.locale_label(43)
'Rural-Remote'
Parameters:

code (int)

Return type:

str

siege_utilities.geo.locale.locale_from_code(code)[source]

Fast lookup of a pre-built LocaleCode by integer code.

Returns the pre-built constant when possible, avoiding dataclass construction.

Parameters:

code (int)

Return type:

LocaleCode

Runtime capability detection for the geo subsystem.

Reports which optional spatial packages are available so callers can adapt gracefully (e.g. fall back to geo-lite operations when geopandas is absent).

siege_utilities.geo.capabilities.geo_capabilities()[source]

Detect available spatial packages and report the active tier.

Returns a dict with boolean flags for each optional package and a tier string ("geodjango", "geo", "geo-lite", or "none").

>>> caps = geo_capabilities()
>>> caps["tier"] in ("geodjango", "geo", "geo-lite", "none")
True
>>> isinstance(caps["shapely"], bool)
True
Return type:

Dict[str, Any]

Boundaries and Providers

Deprecation shim — re-exports from siege_utilities.geo.providers.boundary_providers.

Moved during ELE-2438 (spatial providers consolidated under geo/providers/). Will be removed in v4.0.0.

exception siege_utilities.geo.boundary_providers.BoundaryFetchError[source]

Bases: RuntimeError

Raised when a boundary provider cannot satisfy a request after retries.

class siege_utilities.geo.boundary_providers.BoundaryProvider[source]

Bases: ABC

Abstract base class for geographic boundary data providers.

abstract property provider_name: str

Human-readable name for this provider.

abstractmethod get_boundary(level, identifier=None, **kwargs)[source]

Fetch boundary geometry for a given geographic level.

Parameters:
  • level (str) – Geographic level (e.g. ‘county’, ‘tract’, ‘admin1’).

  • identifier (str | None) – Optional identifier to narrow the query (e.g. state FIPS).

  • **kwargs (Any) –

    Provider-specific options. Recognized cross-provider kwargs: engine: Optional DataFrameEngine instance. When provided,

    the result is converted to the engine’s native format via engine.from_geodataframe(). Default (None) returns a GeoDataFrame.

Returns:

GeoDataFrame when engine is None; engine-native DataFrame otherwise.

Return type:

Any

abstractmethod list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

abstractmethod is_available()[source]

Return True if this provider’s dependencies are installed and reachable.

Return type:

bool

class siege_utilities.geo.boundary_providers.CensusTIGERProvider[source]

Bases: BoundaryProvider

US Census TIGER/Line boundary provider.

Wraps siege_utilities.geo.spatial_data.CensusDataSource and siege_utilities.config.census_constants.CANONICAL_GEOGRAPHIC_LEVELS.

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch US Census TIGER boundaries.

Parameters:
  • level (str) – A canonical geographic level (e.g. ‘county’, ‘tract’, ‘cd’).

  • identifier (str | None) – State FIPS code when the level requires it.

  • **kwargs (Any) – Forwarded to CensusDataSource.fetch_geographic_boundaries (e.g. year, congress_number). engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:

BoundaryFetchError – When the boundary retrieval fails.

list_levels()[source]

Return canonical Census geographic level names.

Return type:

list[str]

is_available()[source]

Census TIGER provider is always available (pure-HTTP downloads).

Return type:

bool

list_available_vintages(timeout=30)[source]

Fetch the Census TIGER listing and parse out published TIGER{YYYY} years.

Returns a sorted list of int years.

Raises:

requests.RequestException – On network failure.

Parameters:

timeout (int)

Return type:

list[int]

latest_vintage(timeout=30)[source]

Return the newest published TIGER vintage year, or None on failure.

Parameters:

timeout (int)

Return type:

int | None

class siege_utilities.geo.boundary_providers.GADMProvider[source]

Bases: BoundaryProvider

GADM (Global Administrative Areas) boundary provider.

Downloads GeoJSON boundary files from the GADM project for non-US countries. Requires geopandas at runtime.

__init__(version='4.1')[source]
Parameters:

version (str)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch GADM boundaries for a country.

Parameters:
  • level (str) – One of ‘country’, ‘admin1’, ‘admin2’, ‘admin3’.

  • identifier (str | None) – ISO-3 country code (e.g. ‘DEU’, ‘FRA’). Can also be passed as country_code kwarg.

  • **kwargs (Any) – country_code accepted as an alias for identifier. engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:
  • ValueError – If level is unknown or country_code is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return GADM administrative levels.

Return type:

list[str]

is_available()[source]

Return True if geopandas is importable.

Return type:

bool

class siege_utilities.geo.boundary_providers.RDHProvider[source]

Bases: BoundaryProvider

Redistricting Data Hub boundary provider.

Wraps siege_utilities.geo.providers.redistricting_data_hub.RDHClient to expose precinct / VTD boundaries — and enacted legislative plans — through the standard BoundaryProvider interface.

RDH requires a “designated API user” account. Contact info@redistrictingdatahub.org to request access. Pass credentials either via constructor arguments or environment variables RDH_USERNAME / RDH_PASSWORD.

Supported levels

  • 'precinct' — precinct/VTD boundaries with election results

  • 'congress' — enacted congressional district plans

  • 'state_senate' — enacted upper-chamber plans

  • 'state_house' — enacted lower-chamber plans

Usage:

import os
from siege_utilities.geo.providers.boundary_providers import RDHProvider

# Prefer env vars (RDH_USERNAME / RDH_PASSWORD); never hardcode
provider = RDHProvider(
    username=os.environ["RDH_USERNAME"],
    password=os.environ["RDH_PASSWORD"],
)
gdf = provider.get_boundary('precinct', identifier='TX')
__init__(username=None, password=None, cache_dir=None)[source]
Parameters:
  • username (str | None)

  • password (str | None)

  • cache_dir (str | None)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch RDH boundary data for a state.

Parameters:
  • level (str) – One of 'precinct', 'congress', 'state_senate', 'state_house'.

  • identifier (str | None) – Two-letter state abbreviation (e.g. 'TX'). Can also be passed as state kwarg.

  • **kwargs (Any) – state — alias for identifier. year — filter datasets by year string (e.g. '2022'). format'shp' (default) or 'csv'. engine — Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided. None if no matching datasets found.

Raises:
  • ValueError – If level is not recognised or state is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

is_available()[source]

Return True if geopandas is installed and both credentials are set.

Return type:

bool

siege_utilities.geo.boundary_providers.resolve_boundary_provider(country='US', **kwargs)[source]

Return an appropriate BoundaryProvider for the given country.

Parameters:
  • country (str) – ISO-2 or ISO-3 country code (default 'US').

  • **kwargs (Any) – Forwarded to GADMProvider constructor for non-US countries. Ignored for US (CensusTIGERProvider takes no options).

Returns:

CensusTIGERProvider for US / US territories, GADMProvider otherwise.

Return type:

BoundaryProvider

Structured diagnostics for Census boundary retrieval.

Provides typed exceptions and a result object so callers can programmatically branch on failure reason without parsing free-text logs.

Usage:

from siege_utilities.geo import fetch_geographic_boundaries

result = fetch_geographic_boundaries(2020, 'county', state_fips='48')
if result.success:
    gdf = result.geodataframe
else:
    print(f"Failed at stage '{result.error_stage}': {result.message}")
    print(f"Context: {result.context}")
exception siege_utilities.geo.boundary_result.BoundaryConfigurationError[source]

Bases: BoundaryRetrievalError

Boundary type requires parameters that were not provided (e.g., state FIPS, congress number).

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.boundary_result.BoundaryDiscoveryError[source]

Bases: BoundaryRetrievalError

Failed to discover available boundary types or construct a URL.

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.boundary_result.BoundaryDownloadError[source]

Bases: BoundaryRetrievalError

Download succeeded but the file is corrupt or not a valid zip.

__init__(message, context=None)[source]
Parameters:
class siege_utilities.geo.boundary_result.BoundaryFetchResult[source]

Bases: object

Structured result from a boundary retrieval attempt.

success

Whether the retrieval succeeded.

Type:

bool

geodataframe

The resulting GeoDataFrame (None on failure).

Type:

Any

error_code

Machine-readable error code (None on success).

Type:

str | None

error_stage

Pipeline stage where failure occurred (None on success). One of: input_validation, discovery, url_validation, download, parse.

Type:

str | None

message

Human-readable description of the outcome.

Type:

str

context

Diagnostic details (attempted URLs, year fallback, HTTP status, etc.).

Type:

Dict[str, Any]

success: bool
geodataframe: Any = None
error_code: str | None = None
error_stage: str | None = None
message: str = ''
context: Dict[str, Any]
raise_on_error()[source]

Raise the appropriate typed exception if this result is a failure.

Returns self on success so callers can chain:

gdf = fetch_geographic_boundaries(...).raise_on_error().geodataframe
Return type:

BoundaryFetchResult

classmethod ok(gdf, message='', context=None)[source]

Construct a success result.

Parameters:
Return type:

BoundaryFetchResult

classmethod fail(error_code, error_stage, message, context=None)[source]

Construct a failure result.

Parameters:
Return type:

BoundaryFetchResult

__init__(success, geodataframe=None, error_code=None, error_stage=None, message='', context=<factory>)
Parameters:
Return type:

None

exception siege_utilities.geo.boundary_result.BoundaryInputError[source]

Bases: BoundaryRetrievalError

Invalid input parameters (state FIPS, year, geographic level).

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.boundary_result.BoundaryParseError[source]

Bases: BoundaryRetrievalError

Downloaded data could not be parsed as a shapefile/GeoDataFrame.

__init__(message, context=None)[source]
Parameters:
exception siege_utilities.geo.boundary_result.BoundaryRetrievalError[source]

Bases: SiegeGeoError

Base exception for all boundary retrieval failures.

Inherits from SiegeGeoError so callers can catch the entire siege_utilities exception family with a single except SiegeError:. Previously this stood alone outside the documented hierarchy and slipped past except SiegeError blocks.

__init__(message, stage, context=None)[source]
Parameters:
exception siege_utilities.geo.boundary_result.BoundaryUrlValidationError[source]

Bases: BoundaryRetrievalError

Constructed URL is not accessible (HTTP error, timeout, etc.).

__init__(message, context=None)[source]
Parameters:

Boundary data source convenience functions (engine-agnostic).

Thin wrappers that know about specific data sources — Census TIGER, GADM, RDH redistricting plans. These call engine.load_polygons() with the correct paths and parameters.

Works with ANY DataFrameEngine (Pandas, DuckDB, Spark, PostGIS). For custom boundary sources, call engine.load_polygons() directly.

Usage:

from siege_utilities.engines.dataframe_engine import get_engine, Engine
from siege_utilities.geo.boundary_sources import load_census_boundaries

engine = get_engine(Engine.SPARK, enable_sedona=True)

# Load congressional districts via the engine
districts = load_census_boundaries(engine, "cd", year=2020)

# Assign addresses to districts
enriched = engine.assign_boundaries(addresses, districts)
siege_utilities.geo.boundary_sources.load_census_boundaries(engine, boundary_type, year=2020, state_fips=None, s3_base='s3a://boundaries/tiger')[source]

Load Census TIGER boundaries via any DataFrameEngine.

Parameters:
  • engine (DataFrameEngine) – Any engine instance (Pandas, DuckDB, Spark, PostGIS).

  • boundary_type (str) – One of: state, county, tract, blockgroup, block, place, zcta, cd, sldu, sldl, vtd, cbsa.

  • year (int) – Census vintage year (default 2020).

  • state_fips (str, optional) – 2-digit state FIPS to filter.

  • s3_base (str) – Base path for staged GeoParquet.

Return type:

Engine-native DataFrame with geometry column.

siege_utilities.geo.boundary_sources.load_gadm_boundaries(engine, level=0, country=None, s3_base='s3a://boundaries/gadm')[source]

Load GADM (Global Administrative Areas) boundaries via any engine.

Parameters:
  • engine (DataFrameEngine)

  • level (int) – Admin level 0-5 (0=countries, 1=states/provinces, etc.)

  • country (str, optional) – ISO 3166-1 alpha-3 country code to filter.

  • s3_base (str) – Base path for staged GADM data.

Return type:

Any

siege_utilities.geo.boundary_sources.load_rdh_boundaries(engine, state, plan_type='enacted', s3_base='s3a://boundaries/rdh')[source]

Load Redistricting Data Hub plan boundaries via any engine.

Parameters:
  • engine (DataFrameEngine)

  • state (str) – 2-letter state abbreviation.

  • plan_type (str) – ‘enacted’, ‘proposed’, ‘alternative’.

  • s3_base (str)

Return type:

Any

siege_utilities.geo.boundary_sources.stage_census_boundaries(engine, boundary_type, year=2020, s3_base='s3a://boundaries/tiger')[source]

Stage Census TIGER boundaries for fast loading.

Fetches from Census API via CensusTIGERProvider, converts geometry to the engine’s format, writes to the staging path.

Parameters:
Returns:

str

Return type:

path where boundaries were written.

External spatial data sources — one folder, one shape per provider.

This is the canonical home for every module that fetches spatial data from an external source. Housed here under ELE-2438 (D3) as the first step toward a unified provider interface.

Current providers:

  • boundary_providers — CensusTIGER, GADM, RDH boundary retrieval

  • census_geocoder — US Census geocoder (address → FIPS)

  • nces_download — NCES school-district and locale boundaries

  • redistricting_data_hub — RDH API client (precincts, plans, CVAP, PL 94-171, compactness metrics)

  • nlrb_clients — NLRB case data from data.gov, labordata GitHub, and NxGen search

Not yet migrated (planned for the interface-unification follow-up epic):

  • geo/spatial_data.pyCensusDataSource + GovernmentDataSource + OpenStreetMapDataSource. File is 1,500+ lines mixing concerns; will be split as part of the provider-interface work.

  • geo/census/ subpackage — Census API client. Moves once the variable-registry split stabilizes.

The “structural moves only” scope of ELE-2438 does not unify these providers behind a common fetch / accepts interface — that’s a separate, larger effort.

Boundary provider abstraction for geographic boundary data.

Provides a pluggable architecture for fetching administrative boundary geometries from different sources:

  • CensusTIGERProvider: US boundaries via Census TIGER/Line shapefiles

  • GADMProvider: International boundaries via the Database of Global Administrative Areas

  • resolve_boundary_provider(): Factory that selects the appropriate provider by country

Usage:

from siege_utilities.geo.providers.boundary_providers import resolve_boundary_provider

provider = resolve_boundary_provider('US')
gdf = provider.get_boundary('county', state_fips='06')

intl_provider = resolve_boundary_provider('DE')
gdf = intl_provider.get_boundary('admin1', country_code='DEU')
exception siege_utilities.geo.providers.boundary_providers.BoundaryFetchError[source]

Bases: RuntimeError

Raised when a boundary provider cannot satisfy a request after retries.

class siege_utilities.geo.providers.boundary_providers.BoundaryProvider[source]

Bases: ABC

Abstract base class for geographic boundary data providers.

abstract property provider_name: str

Human-readable name for this provider.

abstractmethod get_boundary(level, identifier=None, **kwargs)[source]

Fetch boundary geometry for a given geographic level.

Parameters:
  • level (str) – Geographic level (e.g. ‘county’, ‘tract’, ‘admin1’).

  • identifier (str | None) – Optional identifier to narrow the query (e.g. state FIPS).

  • **kwargs (Any) –

    Provider-specific options. Recognized cross-provider kwargs: engine: Optional DataFrameEngine instance. When provided,

    the result is converted to the engine’s native format via engine.from_geodataframe(). Default (None) returns a GeoDataFrame.

Returns:

GeoDataFrame when engine is None; engine-native DataFrame otherwise.

Return type:

Any

abstractmethod list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

abstractmethod is_available()[source]

Return True if this provider’s dependencies are installed and reachable.

Return type:

bool

class siege_utilities.geo.providers.boundary_providers.CensusTIGERProvider[source]

Bases: BoundaryProvider

US Census TIGER/Line boundary provider.

Wraps siege_utilities.geo.spatial_data.CensusDataSource and siege_utilities.config.census_constants.CANONICAL_GEOGRAPHIC_LEVELS.

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch US Census TIGER boundaries.

Parameters:
  • level (str) – A canonical geographic level (e.g. ‘county’, ‘tract’, ‘cd’).

  • identifier (str | None) – State FIPS code when the level requires it.

  • **kwargs (Any) – Forwarded to CensusDataSource.fetch_geographic_boundaries (e.g. year, congress_number). engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:

BoundaryFetchError – When the boundary retrieval fails.

list_levels()[source]

Return canonical Census geographic level names.

Return type:

list[str]

is_available()[source]

Census TIGER provider is always available (pure-HTTP downloads).

Return type:

bool

list_available_vintages(timeout=30)[source]

Fetch the Census TIGER listing and parse out published TIGER{YYYY} years.

Returns a sorted list of int years.

Raises:

requests.RequestException – On network failure.

Parameters:

timeout (int)

Return type:

list[int]

latest_vintage(timeout=30)[source]

Return the newest published TIGER vintage year, or None on failure.

Parameters:

timeout (int)

Return type:

int | None

class siege_utilities.geo.providers.boundary_providers.GADMProvider[source]

Bases: BoundaryProvider

GADM (Global Administrative Areas) boundary provider.

Downloads GeoJSON boundary files from the GADM project for non-US countries. Requires geopandas at runtime.

__init__(version='4.1')[source]
Parameters:

version (str)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch GADM boundaries for a country.

Parameters:
  • level (str) – One of ‘country’, ‘admin1’, ‘admin2’, ‘admin3’.

  • identifier (str | None) – ISO-3 country code (e.g. ‘DEU’, ‘FRA’). Can also be passed as country_code kwarg.

  • **kwargs (Any) – country_code accepted as an alias for identifier. engine: Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided.

Raises:
  • ValueError – If level is unknown or country_code is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return GADM administrative levels.

Return type:

list[str]

is_available()[source]

Return True if geopandas is importable.

Return type:

bool

class siege_utilities.geo.providers.boundary_providers.RDHProvider[source]

Bases: BoundaryProvider

Redistricting Data Hub boundary provider.

Wraps siege_utilities.geo.providers.redistricting_data_hub.RDHClient to expose precinct / VTD boundaries — and enacted legislative plans — through the standard BoundaryProvider interface.

RDH requires a “designated API user” account. Contact info@redistrictingdatahub.org to request access. Pass credentials either via constructor arguments or environment variables RDH_USERNAME / RDH_PASSWORD.

Supported levels

  • 'precinct' — precinct/VTD boundaries with election results

  • 'congress' — enacted congressional district plans

  • 'state_senate' — enacted upper-chamber plans

  • 'state_house' — enacted lower-chamber plans

Usage:

import os
from siege_utilities.geo.providers.boundary_providers import RDHProvider

# Prefer env vars (RDH_USERNAME / RDH_PASSWORD); never hardcode
provider = RDHProvider(
    username=os.environ["RDH_USERNAME"],
    password=os.environ["RDH_PASSWORD"],
)
gdf = provider.get_boundary('precinct', identifier='TX')
__init__(username=None, password=None, cache_dir=None)[source]
Parameters:
  • username (str | None)

  • password (str | None)

  • cache_dir (str | None)

Return type:

None

property provider_name: str

Human-readable name for this provider.

get_boundary(level, identifier=None, **kwargs)[source]

Fetch RDH boundary data for a state.

Parameters:
  • level (str) – One of 'precinct', 'congress', 'state_senate', 'state_house'.

  • identifier (str | None) – Two-letter state abbreviation (e.g. 'TX'). Can also be passed as state kwarg.

  • **kwargs (Any) – state — alias for identifier. year — filter datasets by year string (e.g. '2022'). format'shp' (default) or 'csv'. engine — Optional DataFrameEngine for result conversion.

Returns:

GeoDataFrame (default) or engine-native DataFrame when engine is provided. None if no matching datasets found.

Raises:
  • ValueError – If level is not recognised or state is missing.

  • ImportError – If geopandas is not installed.

list_levels()[source]

Return the geographic levels this provider supports.

Return type:

list[str]

is_available()[source]

Return True if geopandas is installed and both credentials are set.

Return type:

bool

siege_utilities.geo.providers.boundary_providers.resolve_boundary_provider(country='US', **kwargs)[source]

Return an appropriate BoundaryProvider for the given country.

Parameters:
  • country (str) – ISO-2 or ISO-3 country code (default 'US').

  • **kwargs (Any) – Forwarded to GADMProvider constructor for non-US countries. Ignored for US (CensusTIGERProvider takes no options).

Returns:

CensusTIGERProvider for US / US territories, GADMProvider otherwise.

Return type:

BoundaryProvider

Census Bureau Geocoder integration with historical vintage support.

Wraps the Census Bureau’s geocoding API for US address geocoding. Returns FIPS codes directly (state, county, tract, block) so most addresses don’t need a spatial join. Supports historical vintages for temporal accuracy.

Usage:
from siege_utilities.geo import (

CensusVintage, select_vintage_for_cycle, geocode_single, geocode_batch, CensusGeocodeResult,

)

# Single address result = geocode_single(“1600 Pennsylvania Ave”, “Washington”, “DC”, “20500”)

# Batch (up to 10,000) results = geocode_batch(addresses, vintage=CensusVintage.CENSUS_2020)

# Auto-select vintage for an FEC cycle year vintage = select_vintage_for_cycle(2018) # -> CensusVintage.CENSUS_2020

exception siege_utilities.geo.providers.census_geocoder.CensusGeocodeError[source]

Bases: RuntimeError

Raised when the Census geocoder API call fails unexpectedly.

Distinct from “no match” results (which return a CensusGeocodeResult with matched=False). This exception indicates an API / network / parse failure where the geocoder could not even attempt to match. Use __cause__ to inspect the underlying exception.

class siege_utilities.geo.providers.census_geocoder.CensusGeocodeResult[source]

Bases: object

Result from Census Bureau geocoding.

matched

Whether the address was matched.

Type:

bool

input_address

The original input address string.

Type:

str

matched_address

The standardized matched address (if matched).

Type:

str

lat

Latitude (if matched).

Type:

float | None

lon

Longitude (if matched).

Type:

float | None

state_fips

2-digit state FIPS code.

Type:

str

county_fips

3-digit county FIPS code.

Type:

str

tract

6-digit tract code.

Type:

str

block

4-digit block code.

Type:

str

match_type

“Exact”, “Non_Exact”, or “No_Match”.

Type:

str

side

Street side (“L” or “R”, from TIGER).

Type:

str

tiger_line_id

TIGER/Line feature ID.

Type:

str

matched: bool = False
input_address: str = ''
matched_address: str = ''
lat: float | None = None
lon: float | None = None
state_fips: str = ''
county_fips: str = ''
tract: str = ''
block: str = ''
match_type: str = 'No_Match'
side: str = ''
tiger_line_id: str = ''
input_id: str = ''
property state_geoid: str

2-digit state GEOID.

property county_geoid: str

5-digit county GEOID (state + county FIPS).

property tract_geoid: str

11-digit tract GEOID (state + county + tract).

property block_geoid: str

15-digit block GEOID (state + county + tract + block).

property block_group_geoid: str

12-digit block group GEOID (block GEOID truncated to 12 chars).

__init__(matched=False, input_address='', matched_address='', lat=None, lon=None, state_fips='', county_fips='', tract='', block='', match_type='No_Match', side='', tiger_line_id='', input_id='')
Parameters:
Return type:

None

class siege_utilities.geo.providers.census_geocoder.CensusVintage[source]

Bases: str, Enum

Census geocoder benchmark/vintage pairs.

Each member’s value encodes <benchmark>|<vintage> where both halves are the exact strings the Census Geocoder API expects. Use the .benchmark and .vintage properties to extract them.

The benchmark names follow the Public_AR_<label> pattern used by https://geocoding.geo.census.gov/geocoder/benchmarks. The vintage names follow the <label>_<benchmark-label> pattern returned by https://geocoding.geo.census.gov/geocoder/vintages?benchmark=….

CENSUS_2010 = 'Public_AR_Census2020|Census2010_Census2020'
CENSUS_2020 = 'Public_AR_Census2020|Census2020_Census2020'
CURRENT = 'Public_AR_Current|Current_Current'
ACS_2022 = 'Public_AR_Current|ACS2022_Current'
ACS_2023 = 'Public_AR_Current|ACS2023_Current'
property benchmark: str

The benchmark string accepted by the Census Geocoder API.

property vintage: str

The vintage string accepted by the Census Geocoder API.

__new__(value)
siege_utilities.geo.providers.census_geocoder.geocode_batch(addresses, vintage=CensusVintage.CURRENT)[source]

Geocode a batch of addresses via the Census Bureau batch API.

The Census batch API accepts up to 10,000 addresses per request. Each address dict should have keys: id, street, city, state, zipcode.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

List of CensusGeocodeResult, one per input address (order preserved).

Raises:

ValueError – If batch exceeds 10,000 addresses.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.providers.census_geocoder.geocode_batch_chunked(addresses, vintage=CensusVintage.CURRENT, chunk_size=10000)[source]

Geocode addresses in chunks of up to chunk_size (default 10,000).

Convenience wrapper around geocode_batch() for larger datasets.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

  • chunk_size (int) – Max addresses per API call (max 10,000).

Returns:

List of CensusGeocodeResult, one per input address.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.providers.census_geocoder.geocode_results_to_dataframe(results)[source]

Convert a list of CensusGeocodeResult to a pandas DataFrame.

Extracts all dataclass fields and computed GEOID properties into a flat DataFrame with standard column names. This eliminates the per-field list-comprehension boilerplate that every caller of geocode_batch() otherwise has to write.

Parameters:

results (list[CensusGeocodeResult]) – List of CensusGeocodeResult objects, typically returned by geocode_batch() or geocode_batch_chunked().

Returns:

input_id, input_address, matched, match_type, matched_address, latitude, longitude, state_geoid, county_geoid, tract_geoid, block_geoid, block_group_geoid, state_fips, county_fips, tract, block, side, tiger_line_id. For unmatched rows, latitude/longitude are None and GEOID columns are empty strings.

Return type:

DataFrame with columns

Raises:

TypeError – If results is not a list.

Example:

results = geocode_batch(addresses)
df = geocode_results_to_dataframe(results)
# df now has tract_geoid, block_geoid, lat/lon, etc.
siege_utilities.geo.providers.census_geocoder.geocode_single(street, city, state, zipcode, vintage=CensusVintage.CURRENT)[source]

Geocode a single address via the Census Bureau API.

Parameters:
  • street (str) – Street address (e.g., “1600 Pennsylvania Ave NW”).

  • city (str) – City name.

  • state (str) – State abbreviation or name.

  • zipcode (str) – ZIP code.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

CensusGeocodeResult with lat/lon and FIPS codes if matched.

Return type:

CensusGeocodeResult

siege_utilities.geo.providers.census_geocoder.select_vintage_for_cycle(year)[source]

Select the appropriate Census vintage for an FEC cycle year.

Parameters:

year (int) – The FEC election cycle year.

Returns:

CensusVintage matching the boundaries in effect for that cycle.

Return type:

CensusVintage

Examples

>>> select_vintage_for_cycle(2024)
<CensusVintage.CURRENT: 'Public_AR_Current|Current_Current'>
>>> select_vintage_for_cycle(2016)
<CensusVintage.CENSUS_2020: 'Public_AR_Census2020|Census2020_Census2020'>
>>> select_vintage_for_cycle(2008)
<CensusVintage.CENSUS_2010: 'Public_AR_Census2020|Census2010_Census2020'>

Composite batch geocoder with fallback chaining.

Chains multiple BatchGeocoder backends by priority, falling back to the next backend for addresses that fail or match below a quality threshold.

Typical usage: Census first (free, returns GEOIDs), then Nominatim or TAMU for failures.

class siege_utilities.geo.providers.composite_geocoder.CompositeBatchGeocoder[source]

Bases: BatchGeocoder

Chains geocoding backends by priority with fallback.

For each address, tries backends in order. If a backend returns a match below the minimum quality threshold, the next backend is tried. The best result across all backends is kept.

Parameters:
  • backends – Ordered list of geocoder backends (highest priority first).

  • min_quality – Minimum match quality to accept without fallback. Default “approximate” — only “centroid” and “no_match” trigger fallback.

  • skip_unavailable – Skip backends where is_available() returns False. Default True.

__init__(backends, min_quality='approximate', skip_unavailable=True)[source]
Parameters:
property backend_name: str

Short identifier for this backend (e.g., ‘census’, ‘nominatim’).

geocode(addresses)[source]

Geocode addresses through chained backends.

Each address is tried against backends in order until a result meeting the quality threshold is found, or all backends are exhausted. The best result is kept.

Parameters:

addresses (list[dict] | list[str] | list[AddressInput])

Return type:

BatchGeocodingResult

Unified batch geocoding interface.

Defines the BatchGeocoder abstract base class and GeocodingResult schema for consistent geocoding across multiple backends (Census, Nominatim, TAMU, etc.).

Usage:

from siege_utilities.geo.providers.batch_geocoder import (
    BatchGeocoder, GeocodingResult, MatchQuality,
)
class siege_utilities.geo.providers.batch_geocoder.AddressInput[source]

Bases: object

Normalized address input for batch geocoding.

street: str = ''
city: str = ''
state: str = ''
zipcode: str = ''
input_id: str = ''
property one_line: str
__init__(street='', city='', state='', zipcode='', input_id='')
Parameters:
Return type:

None

class siege_utilities.geo.providers.batch_geocoder.BatchGeocoder[source]

Bases: ABC

Abstract base class for batch geocoding backends.

Subclasses implement geocode() to process a list of addresses through a specific geocoding service. The interface accepts flexible input formats and returns a BatchGeocodingResult.

Example:

class MyGeocoder(BatchGeocoder):
    @property
    def backend_name(self) -> str:
        return "my_service"

    def geocode(self, addresses, **kwargs):
        normalized = normalize_addresses(addresses)
        results = []
        for addr in normalized:
            # ... call service ...
            results.append(GeocodingResult(...))
        return BatchGeocodingResult(results=results, backend=self.backend_name)
abstract property backend_name: str

Short identifier for this backend (e.g., ‘census’, ‘nominatim’).

abstractmethod geocode(addresses)[source]

Geocode a batch of addresses.

Parameters:

addresses (list[dict] | list[str] | list[AddressInput]) – List of addresses in any supported format.

Returns:

BatchGeocodingResult with one GeocodingResult per input address.

Return type:

BatchGeocodingResult

is_available()[source]

Check if this backend is currently available.

Default returns True. Override for backends that need API keys or network connectivity checks.

Return type:

bool

class siege_utilities.geo.providers.batch_geocoder.BatchGeocodingResult[source]

Bases: object

Container for a batch geocoding run.

results: list[GeocodingResult]
backend: str = ''
errors: list[str]
property total: int
property matched_count: int
property match_rate: float
to_dataframe()[source]

Convert results to a pandas DataFrame.

Returns:

DataFrame with one row per result, including all fields.

to_geodataframe()[source]

Convert results to a GeoDataFrame with point geometry.

Only includes matched results (those with lat/lon coordinates).

Returns:

Return type:

GeoDataFrame with point geometry in EPSG

__init__(results=<factory>, backend='', errors=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.providers.batch_geocoder.GeocodingResult[source]

Bases: object

Unified geocoding result across all backends.

input_address

Original address as submitted.

Type:

str

input_id

Caller-supplied identifier (preserved through geocoding).

Type:

str

matched_address

Standardized matched address from the geocoder.

Type:

str

lat

Latitude of the matched location.

Type:

float | None

lon

Longitude of the matched location.

Type:

float | None

match_quality

How precise the match is.

Type:

str

block_geoid

15-digit Census block GEOID (if available).

Type:

str

tract_geoid

11-digit Census tract GEOID (if available).

Type:

str

county_geoid

5-digit county GEOID (if available).

Type:

str

state_geoid

2-digit state GEOID (if available).

Type:

str

backend

Name of the geocoding backend that produced this result.

Type:

str

input_address: str = ''
input_id: str = ''
matched_address: str = ''
lat: float | None = None
lon: float | None = None
match_quality: str = 'no_match'
block_geoid: str = ''
tract_geoid: str = ''
county_geoid: str = ''
state_geoid: str = ''
backend: str = ''
property matched: bool
property has_block: bool
property has_tract: bool
to_dict()[source]
Return type:

dict

__init__(input_address='', input_id='', matched_address='', lat=None, lon=None, match_quality='no_match', block_geoid='', tract_geoid='', county_geoid='', state_geoid='', backend='')
Parameters:
  • input_address (str)

  • input_id (str)

  • matched_address (str)

  • lat (float | None)

  • lon (float | None)

  • match_quality (str)

  • block_geoid (str)

  • tract_geoid (str)

  • county_geoid (str)

  • state_geoid (str)

  • backend (str)

Return type:

None

class siege_utilities.geo.providers.batch_geocoder.MatchQuality[source]

Bases: str, Enum

Geocoding match quality levels.

EXACT = 'exact'
INTERPOLATED = 'interpolated'
APPROXIMATE = 'approximate'
CENTROID = 'centroid'
NO_MATCH = 'no_match'
__new__(value)
siege_utilities.geo.providers.batch_geocoder.normalize_addresses(addresses)[source]

Normalize address input to a list of AddressInput.

Accepts: - List of dicts with keys: street, city, state, zipcode, id (optional) - List of one-line address strings - List of AddressInput instances (returned as-is)

Parameters:

addresses (list[dict] | list[str] | list[AddressInput])

Return type:

list[AddressInput]

Census Bureau batch geocoder backend.

Wraps geocode_batch_chunked() behind the BatchGeocoder ABC, converting CensusGeocodeResult to the unified GeocodingResult schema.

class siege_utilities.geo.providers.census_batch.CensusBatchGeocoder[source]

Bases: BatchGeocoder

Census Bureau batch geocoding backend.

Wraps the existing Census geocoder functions with automatic chunking for datasets larger than 10,000 addresses. Returns block-level GEOIDs directly from the Census API (no spatial join needed).

Parameters:
  • vintage – Census vintage to use. Defaults to Current.

  • chunk_size – Max addresses per API call. Default 10,000.

__init__(vintage=None, chunk_size=10000)[source]
Parameters:

chunk_size (int)

property backend_name: str

Short identifier for this backend (e.g., ‘census’, ‘nominatim’).

geocode(addresses)[source]

Geocode addresses via the Census Bureau batch API.

Parameters:

addresses (list[dict] | list[str] | list[AddressInput]) – Addresses in any supported format.

Return type:

BatchGeocodingResult

Nominatim batch geocoder backend.

Wraps Nominatim geocoding (public or self-hosted) behind the BatchGeocoder ABC with configurable rate limiting.

Nominatim does not return Census GEOIDs — those must be assigned via spatial join if needed.

class siege_utilities.geo.providers.nominatim_batch.NominatimBatchGeocoder[source]

Bases: BatchGeocoder

Nominatim geocoding backend with rate limiting.

Uses the existing geocoding infrastructure to query Nominatim one address at a time (Nominatim has no true batch API).

Parameters:
  • server_url – Nominatim endpoint. Defaults to public instance.

  • rate_limit – Seconds between requests. Default 1.0 for public, 0.05 for self-hosted (auto-detected from URL).

  • country_codes – Restrict results to these countries (e.g., [‘us’]).

  • max_retries – Max retries per address on transient failure.

__init__(server_url=None, rate_limit=None, country_codes=None, max_retries=2)[source]
Parameters:
  • server_url (str | None)

  • rate_limit (float | None)

  • country_codes (list[str] | None)

  • max_retries (int)

property backend_name: str

Short identifier for this backend (e.g., ‘census’, ‘nominatim’).

geocode(addresses)[source]

Geocode addresses via Nominatim (one at a time with rate limiting).

Parameters:

addresses (list[dict] | list[str] | list[AddressInput]) – Addresses in any supported format.

Return type:

BatchGeocodingResult

Texas A&M Geoservices batch geocoder backend.

Wraps the TAMU geocoding API behind the BatchGeocoder ABC. TAMU returns Census geography (state, county, tract, block GEOIDs) directly in its response, so no spatial join is needed.

Requires an API key — get one at https://geoservices.tamu.edu/.

class siege_utilities.geo.providers.tamu_batch.TAMUBatchGeocoder[source]

Bases: BatchGeocoder

Texas A&M Geoservices geocoding backend.

Geocodes addresses one at a time via TAMU’s HTTP API. Returns Census geography (block-level GEOIDs) directly.

Parameters:
  • api_key – TAMU API key. Falls back to TAMU_API_KEY env var.

  • rate_limit – Seconds between requests. Default 0.25.

  • census_year – Census year for geography lookup. Default “2020”.

  • max_retries – Max retries per address on transient failure.

__init__(api_key=None, rate_limit=0.25, census_year='2020', max_retries=2)[source]
Parameters:
  • api_key (str | None)

  • rate_limit (float)

  • census_year (str)

  • max_retries (int)

property backend_name: str

Short identifier for this backend (e.g., ‘census’, ‘nominatim’).

is_available()[source]

Check if this backend is currently available.

Default returns True. Override for backends that need API keys or network connectivity checks.

Return type:

bool

geocode(addresses)[source]

Geocode addresses via the TAMU Geoservices API.

Parameters:

addresses (list[dict] | list[str] | list[AddressInput]) – Addresses in any supported format.

Return type:

BatchGeocodingResult

Natural-language geographic query parsing via Etter.

Wraps the upstream etter package to parse natural-language geographic queries (“5 km north of Lausanne”, “donations near Birmingham”, “tracts in Alabama”) into structured filters.

Why a thin wrapper?

The upstream library hands you a Pydantic GeoQuery object. That’s good shape, but two practical problems for siege_utilities consumers:

  1. Constructing the LLM (langchain ChatOpenAI, ChatAnthropic, etc.) is boilerplate that bleeds into every call site.

  2. Consumers want a shape they can hand to a BoundaryProvider or a DataFrameEngine — not a Pydantic model from an unrelated lib.

This module hides both: EtterParser gives you a default LLM factory and a EtterFilter dataclass that the rest of the geo toolchain can act on.

The LLM dependency is opt-in via the [etter] extra. Etter calls make a network round-trip per query, so this is for ad-hoc exploration / interactive UI / one-off ETL prep — not for batch pipelines where every donor address gets re-parsed.

exception siege_utilities.geo.providers.etter_filter.EtterError[source]

Bases: RuntimeError

Base class for connector-side Etter failures.

exception siege_utilities.geo.providers.etter_filter.EtterParseError[source]

Bases: EtterError

Etter failed to parse the query into a structured filter.

exception siege_utilities.geo.providers.etter_filter.EtterLowConfidenceError[source]

Bases: EtterError

Parser succeeded but confidence is below the threshold.

class siege_utilities.geo.providers.etter_filter.EtterFilter[source]

Bases: object

A normalized parsed query ready for downstream consumption.

Wraps the upstream GeoQuery Pydantic model into a dataclass with the fields siege_utilities consumers actually need. The original upstream object is preserved on raw for callers that need full fidelity.

original_query

The natural-language input verbatim.

Type:

str

spatial_relation

One of the relations Etter recognises — "in", "near", "north_of", "south_of", "east_of", "west_of", etc. None if the query doesn’t carry a relation (e.g. a bare place name).

Type:

str | None

reference_location

The place the relation hangs off — "Lausanne", "Lake Geneva", "Birmingham, AL".

Type:

str | None

buffer_distance_m

Buffer distance in meters if the query had one ("5 km" → 5000). None otherwise.

Type:

float | None

confidence

Overall confidence score in [0, 1].

Type:

float

raw

The upstream etter.GeoQuery object, in case the consumer needs the full breakdown.

Type:

Any

original_query: str
spatial_relation: str | None
reference_location: str | None
buffer_distance_m: float | None = None
confidence: float = 1.0
raw: Any = None
classmethod from_geoquery(original_query, geo_query)[source]

Translate an upstream GeoQuery to an EtterFilter.

Tolerant of upstream field-name drift — uses getattr with defaults rather than positional unpacking.

Parameters:
  • original_query (str)

  • geo_query (Any)

Return type:

EtterFilter

__init__(original_query, spatial_relation, reference_location, buffer_distance_m=None, confidence=1.0, raw=None)
Parameters:
  • original_query (str)

  • spatial_relation (str | None)

  • reference_location (str | None)

  • buffer_distance_m (float | None)

  • confidence (float)

  • raw (Any)

Return type:

None

class siege_utilities.geo.providers.etter_filter.EtterParser[source]

Bases: object

Wrap etter.GeoFilterParser with a sensible default LLM.

Usage:

from siege_utilities.geo.providers.etter_filter import EtterParser

# Defaults: gpt-4o, temperature=0, OPENAI_API_KEY from env.
parser = EtterParser()
result = parser.parse("5 km north of Lausanne")
# result.spatial_relation == "north_of"
# result.reference_location == "Lausanne"
# result.buffer_distance_m == 5000.0

Pass llm= to use a pre-built chat model (e.g. for tests or for pinning to a specific Anthropic / Azure deployment). All other constructor kwargs are forwarded to the upstream parser.

__init__(*, llm=None, confidence_threshold=0.6, strict_mode=False, **upstream_kwargs)[source]
Parameters:
  • llm (Any)

  • confidence_threshold (float)

  • strict_mode (bool)

  • upstream_kwargs (Any)

Return type:

None

parse(query)[source]

Parse query into an EtterFilter.

Raises:
  • EtterParseError – Upstream parser raised an unexpected error. The original exception is chained via __cause__.

  • EtterLowConfidenceError – Parser succeeded but confidence is below the threshold and strict_mode=True. (When strict_mode is False, this case logs and returns normally; check EtterFilter.confidence if the caller needs to gate.) Also raised if a future upstream exposes its own LowConfidenceError and our local check wasn’t reached — we re-raise it as our type so consumers don’t have to know about upstream class names.

Parameters:

query (str)

Return type:

EtterFilter

siege_utilities.geo.providers.etter_filter.default_llm(*, model='gpt-4o', temperature=0.0, api_key=None)[source]

Return a langchain chat model suitable for EtterParser.

Picks the right adapter based on the model name. api_key falls back to the standard environment variable for the chosen provider (OPENAI_API_KEY / ANTHROPIC_API_KEY). Lazy-imports langchain so the rest of this module is importable without it.

Parameters:
  • model (str) – Chat model name (e.g. "gpt-4o", "claude-3-5-sonnet-latest").

  • temperature (float) – Sampling temperature; default 0.0 for reproducibility — query parsing is not creative writing.

  • api_key (str | None) – Optional explicit API key; otherwise read from env.

Return type:

Any

Etter → geometry resolver.

Turns a parsed siege_utilities.geo.providers.etter_filter.EtterFilter into a shapely geometry, using a siege_utilities.geo.gazetteers.Gazetteer to look up the reference location.

The interesting design decision is what geometry “X near Y” means. We ship three modes:

  • RelationSemantics.BOUNDED — default; produces a finite polygon. Directional relations (“north of Y”) are evaluated as a bounded buffer (default_buffer_km × 2) on the directional side of Y’s centroid. This is the safe default for indexed-lookup workloads — the output is always intersectable with a polygon set.

  • RelationSemantics.HALFPLANE — directional relations produce an infinite halfplane. “North of Lake Geneva” → the entire northern halfplane from Lake Geneva’s latitude. Useful for filter semantics where the consumer will intersect with a country / region boundary later. Not ideal for indexed lookups; the geometry is unbounded.

  • RelationSemantics.CONTAINS_CENTROID — instead of returning a region geometry, returns a PointPredicate callable that evaluates contains(candidate.centroid) for each candidate. Use when you want directional filtering without producing a polygon at all.

Buffer-distance precedence:

  1. If the Etter filter parsed an explicit distance (“within 5 km of …”), that wins.

  2. Else, default_buffer_km on the resolver (default 25 km).

class siege_utilities.geo.providers.etter_to_geometry.RelationSemantics[source]

Bases: str, Enum

How to interpret directional/proximity relations.

See module docstring for the full rationale.

BOUNDED = 'bounded'
HALFPLANE = 'halfplane'
CONTAINS_CENTROID = 'contains_centroid'
__new__(value)
class siege_utilities.geo.providers.etter_to_geometry.EtterGeometryResult[source]

Bases: object

The resolved geometry plus diagnostic context.

geometry

The shapely geometry (or PointPredicate for CONTAINS_CENTROID mode).

Type:

Any

relation

The Etter spatial_relation that was applied, or None if the input had no relation (just a bare reference location → the reference geometry itself).

Type:

str | None

reference

The GazetteerResult that anchored the relation. None only when the filter had no reference_location either, which is a degenerate input.

Type:

siege_utilities.geo.gazetteers.base.GazetteerResult | None

buffer_km

Buffer distance actually used (filter wins over default; None for relations that don’t buffer).

Type:

float | None

semantics

Which RelationSemantics mode produced this result.

Type:

siege_utilities.geo.providers.etter_to_geometry.RelationSemantics

notes

Free-text diagnostic notes (e.g., “halfplane truncated to ±90° lat to remain valid GeoJSON”).

Type:

tuple[str, …]

geometry: Any
relation: str | None
reference: GazetteerResult | None
buffer_km: float | None
semantics: RelationSemantics
notes: tuple[str, ...] = ()
__init__(geometry, relation, reference, buffer_km, semantics, notes=())
Parameters:
Return type:

None

exception siege_utilities.geo.providers.etter_to_geometry.EtterToGeometryError[source]

Bases: RuntimeError

Raised when an EtterFilter cannot be turned into a geometry.

Carries the parsed filter and the failure reason. The most common causes — the reference location not being found in the gazetteer, or a relation Etter parsed that we don’t know how to translate — each have their own subclass so callers can branch.

exception siege_utilities.geo.providers.etter_to_geometry.EtterReferenceNotFoundError[source]

Bases: EtterToGeometryError

The reference location couldn’t be resolved by the gazetteer.

exception siege_utilities.geo.providers.etter_to_geometry.EtterUnknownRelationError[source]

Bases: EtterToGeometryError

Etter returned a relation we don’t have a translation for.

siege_utilities.geo.providers.etter_to_geometry.etter_to_geometry(filter_, *, gazetteer, semantics=RelationSemantics.BOUNDED, default_buffer_km=25.0, country_hint=None, admin_hint=None)[source]

Resolve an EtterFilter to a geometry.

Parameters:
  • filter – The parsed query.

  • gazetteer (Gazetteer) – The gazetteer that resolves the reference location. Pass the result of siege_utilities.geo.gazetteers.resolve_gazetteer().

  • semantics (RelationSemantics) – Which mode of relation interpretation to use. The default RelationSemantics.BOUNDED is safe for most indexed-lookup workloads.

  • default_buffer_km (float) – Buffer to apply for “near”-style relations and as the half-width of bounded directional buffers when the filter doesn’t carry an explicit distance.

  • admin_hint (str | None) – Forwarded to gazetteer.lookup to disambiguate the reference location. Etter often emits just the place name; the consumer may know the country from a sibling field.

  • filter_ (EtterFilter)

  • country_hint (str | None)

  • admin_hint

Returns:

EtterGeometryResult with the geometry (or a PointPredicate) and diagnostic context.

Raises:
Return type:

EtterGeometryResult

Redistricting Data Hub (RDH) API client for siege_utilities.

Provides authenticated access to redistrictingdatahub.org datasets: - PL 94-171 redistricting data (population by race at block level) - CVAP special tabulation (citizen voting age population) - Legislative boundary shapefiles (enacted congressional + state leg) - Precinct boundaries with election results - ACS 5-year demographic estimates - Population projections

Authentication requires a “designated API user” account. Contact info@redistrictingdatahub.org to request access.

Usage:

import os
from siege_utilities.geo.providers.redistricting_data_hub import RDHClient

# Prefer env vars (RDH_USERNAME / RDH_PASSWORD); never hardcode
client = RDHClient(
    username=os.environ["RDH_USERNAME"],
    password=os.environ["RDH_PASSWORD"],
)
datasets = client.list_datasets(states=["VA"], format="csv")
df = client.download_dataset(datasets[0])
class siege_utilities.geo.providers.redistricting_data_hub.RDHClient[source]

Bases: object

Client for the Redistricting Data Hub download API.

Parameters:
  • username (str, optional) – RDH account email. Falls back to RDH_USERNAME env var.

  • password (str, optional) – RDH account password. Falls back to RDH_PASSWORD env var.

  • base_url (str, optional) – Override API endpoint (for testing).

  • cache_dir (Path or str, optional) – Directory for caching downloaded files. Defaults to ~/.cache/siege_utilities/rdh/.

__init__(username=None, password=None, base_url='https://redistrictingdatahub.org/wp-json/download/list', cache_dir=None)[source]
Parameters:
  • username (str | None)

  • password (str | None)

  • base_url (str)

  • cache_dir (str | Path | None)

close()[source]

Close the underlying HTTP session.

Return type:

None

credentials_present()[source]

Return True if a username and password are both set.

This is a presence check only – it does NOT contact RDH or verify that the credentials authenticate. A live check is impractical here: the RDH catalog endpoint is WAF-gated to allowlisted ingest hosts (#1115), so from other hosts an authenticated-style request returns the website HTML rather than the JSON API. Callers needing a true liveness check should call list_datasets() and handle the ValueError it now raises on an API error envelope.

Return type:

bool

validate_credentials()[source]

Backward-compatible alias for credentials_present().

Retained for existing callers. The name is a misnomer kept for compatibility: it checks credential presence, not live authentication (see credentials_present()). (#1115)

Return type:

bool

list_datasets(states=None, format=None, year=None, dataset_type=None, geography=None, official=None)[source]

List available datasets with optional filters.

Parameters:
  • states (list of str, optional) – State abbreviations to filter (e.g. ["VA", "MD"]). Due to API limits, queries more than 4 states sequentially.

  • format (str, optional) – "csv" or "shp".

  • year (str, optional) – Four-digit year.

  • dataset_type (str, optional) – Filter keyword in title.

  • geography (str, optional) – Geographic level filter.

  • official (bool, optional) – If True, only return official/enacted datasets.

Return type:

list of RDHDataset

download_dataset(dataset, output_dir=None, use_cache=True)[source]

Download a dataset file.

Parameters:
  • dataset (RDHDataset or str) – Dataset object or direct download URL.

  • output_dir (Path, optional) – Where to save. Defaults to cache_dir.

  • use_cache (bool) – If True, skip download if file already exists.

Return type:

Path to the downloaded file (or extracted directory for ZIPs).

load_csv(dataset, **kwargs)[source]

Download (if needed) and load a CSV dataset into a DataFrame.

Parameters:
  • dataset (RDHDataset, str, or Path) – Dataset to load.

  • **kwargs – Passed to pd.read_csv().

Return type:

pd.DataFrame

load_shapefile(dataset, **kwargs)[source]

Download (if needed) and load a shapefile into a GeoDataFrame.

Parameters:
  • dataset (RDHDataset, str, or Path) – Dataset to load.

  • **kwargs – Passed to gpd.read_file().

Return type:

gpd.GeoDataFrame

get_enacted_plans(state, chamber='congress', year=None, format='shp')[source]

Find enacted redistricting plan datasets for a state.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • chamber (str) – "congress", "state_senate", or "state_house".

  • year (str, optional) – Filter by year.

  • format (str, default "shp") – Dataset format filter. Only "shp" is loadable via load_shapefile(); other formats are returned as metadata only.

Return type:

List[RDHDataset]

get_precinct_data(state, year=None, format='shp')[source]

Find precinct-level datasets for a state.

Parameters:
Return type:

List[RDHDataset]

get_cvap_data(state, year=None)[source]

Find CVAP (Citizen Voting Age Population) datasets.

Parameters:
  • state (str)

  • year (str | None)

Return type:

List[RDHDataset]

get_pl94171_data(state, year=None)[source]

Find PL 94-171 redistricting data datasets.

Parameters:
  • state (str)

  • year (str | None)

Return type:

List[RDHDataset]

static to_crosstab_input(df, geography_col='GEOID', variable_cols=None)[source]

Reshape an RDH DataFrame into long format for cross-tabulation.

Returns a DataFrame with columns [geography, variable, value] suitable for use with siege_utilities.data.cross_tabulation.contingency_table().

Parameters:
  • df (DataFrame) – Wide-format data (one row per geography, numeric columns as variables).

  • geography_col (str) – Column identifying the geographic unit.

  • variable_cols (list of str, optional) – Columns to melt. If None, all numeric columns except the geography column are used.

Return type:

DataFrame with columns geography, variable, value.

class siege_utilities.geo.providers.redistricting_data_hub.RDHDataset[source]

Bases: object

Metadata for a single RDH dataset entry.

title: str
url: str
state: str = ''
format: str = ''
year: str = ''
geography: str = ''
dataset_type: str = ''
official: bool = False
file_size: str = ''
raw: Dict[str, Any]
property filename: str

Extract filename from URL.

property is_shapefile: bool
property is_csv: bool
__init__(title, url, state='', format='', year='', geography='', dataset_type='', official=False, file_size='', raw=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.providers.redistricting_data_hub.RDHDataFormat[source]

Bases: str, Enum

File format for RDH downloads.

CSV = 'csv'
SHAPEFILE = 'shp'
__new__(value)
class siege_utilities.geo.providers.redistricting_data_hub.RDHDatasetType[source]

Bases: str, Enum

Categories of RDH datasets.

PL94171 = 'pl94171'
CVAP = 'cvap'
ACS5 = 'acs5'
ELECTION = 'election'
LEGISLATIVE = 'legislative'
PRECINCT = 'precinct'
VOTER_FILE = 'voter_file'
PROJECTION = 'projection'
__new__(value)
siege_utilities.geo.providers.redistricting_data_hub.polsby_popper(geometry, source_crs=None)[source]

Compute Polsby-Popper compactness score for a geometry.

Score = (4 * pi * area) / perimeter^2 Range: 0 (least compact) to 1 (circle).

Parameters:
  • geometry (shapely geometry) – Must be in a projected (equal-area) CRS for meaningful results.

  • source_crs (optional) – If provided and geographic, geometry is reprojected to equal-area before calculation. Pass the CRS (e.g., “EPSG:4326”) when calling with unprojected data.

Return type:

float

siege_utilities.geo.providers.redistricting_data_hub.reock(geometry, source_crs=None)[source]

Compute Reock compactness score for a geometry.

Score = area / area_of_minimum_bounding_circle Range: 0 (least compact) to 1 (circle).

Parameters:
  • geometry (shapely geometry) – Must be in a projected (equal-area) CRS for meaningful results.

  • source_crs (optional) – If provided and geographic, geometry is reprojected to equal-area.

Return type:

float

siege_utilities.geo.providers.redistricting_data_hub.convex_hull_ratio(geometry, source_crs=None)[source]

Compute convex hull ratio for a geometry.

Score = area / convex_hull_area Range: 0 to 1.

Parameters:
  • geometry (shapely geometry) – Must be in a projected (equal-area) CRS for meaningful results.

  • source_crs (optional) – If provided and geographic, geometry is reprojected to equal-area.

Return type:

float

siege_utilities.geo.providers.redistricting_data_hub.schwartzberg(geometry, source_crs=None)[source]

Compute Schwartzberg compactness score.

Score = 1 / (perimeter / circumference_of_equal_area_circle) Range: 0 to 1.

Parameters:
  • geometry (shapely geometry) – Must be in a projected (equal-area) CRS for meaningful results.

  • source_crs (optional) – If provided and geographic, geometry is reprojected to equal-area.

Return type:

float

siege_utilities.geo.providers.redistricting_data_hub.compute_compactness(gdf, district_id_col='GEOID', geometry_col='geometry')[source]

Compute all compactness metrics for a GeoDataFrame of districts.

Automatically reprojects to an equal-area CRS (Mollweide) before calculating area-dependent metrics.

Parameters:
  • gdf (GeoDataFrame) – Districts with geometry.

  • district_id_col (str) – Column name for district identifiers.

  • geometry_col (str) – Column name for geometry.

Returns:

  • DataFrame with columns (district_id, polsby_popper, reock,)

  • convex_hull_ratio, schwartzberg.

Return type:

pd.DataFrame

class siege_utilities.geo.providers.redistricting_data_hub.CompactnessScores[source]

Bases: object

Compactness metrics for a district geometry.

district_id: str
polsby_popper: float = 0.0
reock: float = 0.0
convex_hull_ratio: float = 0.0
schwartzberg: float = 0.0
__init__(district_id, polsby_popper=0.0, reock=0.0, convex_hull_ratio=0.0, schwartzberg=0.0)
Parameters:
Return type:

None

siege_utilities.geo.providers.redistricting_data_hub.compare_plans(plan_a, plan_b, population_col='TOTPOP', district_id_col='GEOID')[source]

Compare two redistricting plans on key metrics.

Parameters:
  • plan_a (GeoDataFrame) – District plans with population and geometry.

  • plan_b (GeoDataFrame) – District plans with population and geometry.

  • population_col (str) – Column with total population.

  • district_id_col (str) – Column with district identifier.

Return type:

dict with comparison metrics.

siege_utilities.geo.providers.redistricting_data_hub.demographic_profile(plan_gdf, census_gdf, district_id_col='GEOID', population_cols=None)[source]

Overlay Census ACS data onto a redistricting plan.

Performs a spatial join between district geometries and Census block/tract geometries, then aggregates demographics per district.

Parameters:
  • plan_gdf (GeoDataFrame) – District plan with geometries.

  • census_gdf (GeoDataFrame) – Census data with geometries and demographic columns.

  • district_id_col (str) – Column in plan_gdf identifying districts.

  • population_cols (list of str, optional) – Demographic columns to aggregate. Defaults to common ACS fields.

Return type:

DataFrame with one row per district and summed demographic columns.

siege_utilities.geo.providers.redistricting_data_hub.fetch_enacted_plan(state, chamber='congress', year=None, client=None)[source]

Fetch enacted district plan as GeoDataFrame with boundaries + attributes.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • chamber (str) – "congress", "state_senate", or "state_house".

  • year (str, optional) – Filter by year.

  • client (RDHClient, optional) – Existing client instance. Created from env vars if omitted.

Return type:

GeoDataFrame with district boundaries and attributes.

Raises:

FileNotFoundError – If no matching enacted plan dataset is found.

siege_utilities.geo.providers.redistricting_data_hub.fetch_precinct_results(state, year=None, client=None)[source]

Fetch precinct boundaries with election results as GeoDataFrame.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • year (str, optional) – Election year filter.

  • client (RDHClient, optional) – Existing client instance.

Return type:

GeoDataFrame with precinct boundaries and vote columns.

siege_utilities.geo.providers.redistricting_data_hub.fetch_cvap(state, year=None, geography='tract', client=None)[source]

Fetch CVAP (Citizen Voting Age Population) data as DataFrame.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • year (str, optional) – Data year filter.

  • geography (str) – Geographic level hint for filtering (e.g. "tract", "block_group").

  • client (RDHClient, optional) – Existing client instance.

Return type:

DataFrame with CVAP estimates.

siege_utilities.geo.providers.redistricting_data_hub.fetch_pl94171(state, year=None, geography='block', client=None)[source]

Fetch PL 94-171 redistricting population data as DataFrame.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • year (str, optional) – Decennial year filter.

  • geography (str) – Geographic level hint (e.g. "block", "tract").

  • client (RDHClient, optional) – Existing client instance.

Return type:

DataFrame with PL 94-171 population data.

siege_utilities.geo.providers.redistricting_data_hub.fetch_demographic_summary(state, year=None, client=None)[source]

Fetch ACS 5-year demographic summary by district.

Parameters:
  • state (str) – Two-letter state abbreviation.

  • year (str, optional) – ACS year filter.

  • client (RDHClient, optional) – Existing client instance.

Return type:

DataFrame with ACS demographic columns.

NCES data download infrastructure for locale boundaries and school locations.

Downloads pre-computed locale boundary polygons, school geocoded locations, and district administrative data from the NCES EDGE program.

Uses siege_utilities.files.remote for HTTP downloads and caches locally to avoid re-downloading.

exception siege_utilities.geo.providers.nces_download.NCESDownloadError[source]

Bases: Exception

Raised when an NCES download fails.

class siege_utilities.geo.providers.nces_download.NCESDownloader[source]

Bases: object

Download NCES locale boundaries, school locations, and district data.

Downloads from the NCES EDGE (Education Demographic and Geographic Estimates) program and returns data as GeoDataFrames.

Parameters:
  • cache_dir – Directory for caching downloaded files. Defaults to a temporary directory.

  • timeout – HTTP request timeout in seconds.

Example:

downloader = NCESDownloader(cache_dir="./nces_cache")
boundaries = downloader.download_locale_boundaries(2023)
# GeoDataFrame with 12 locale territory polygons
__init__(cache_dir=None, timeout=120)[source]
Parameters:
  • cache_dir (str | None)

  • timeout (int)

download_locale_boundaries(year=2023, *, crs=None)[source]

Download NCES locale boundary polygons.

Returns a GeoDataFrame with 12 rows — one polygon per locale territory (codes 11-43). Each polygon represents the geographic extent of that locale classification.

Parameters:
Returns:

locale_code, locale_category, locale_subcategory, name, geometry.

Return type:

GeoDataFrame with columns

download_school_locations(year=2023, state_abbr=None, *, crs=None)[source]

Download geocoded school locations.

Returns a GeoDataFrame of school point locations with NCES locale codes.

Parameters:
  • year (int) – NCES publication year.

  • state_abbr (str | None) – Optional 2-letter state abbreviation to filter.

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

ncessch, school_name, lea_id, state_abbr, locale_code, locale_category, locale_subcategory, geometry.

Return type:

GeoDataFrame with columns

download_district_data(year=2023)[source]

Download district administrative data with locale codes.

Returns a DataFrame (not GeoDataFrame) of school district administrative records keyed by LEA ID.

Parameters:

year (int) – NCES publication year.

Returns:

lea_id, lea_name, state_abbr, locale_code, locale_category, locale_subcategory, survey_year.

Return type:

DataFrame with columns

NLRB data caching — disk persistence with TTL for NLRBFetchResult.

Stores fetched NLRB case data as JSON in ~/.siege_utilities/cache/nlrb/. The loader orchestrates: cache hit → API fetch → cache write.

class siege_utilities.geo.providers.nlrb_cache.NLRBCache[source]

Bases: object

JSON disk cache for NLRB fetch results.

Parameters:
  • cache_dir – Directory for cache files. Defaults to ~/.siege_utilities/cache/nlrb/.

  • ttl_days – Cache entry lifetime in days. Default 30.

__init__(cache_dir=None, ttl_days=30)[source]
Parameters:
  • cache_dir (Path | None)

  • ttl_days (int)

get(key)[source]

Load a cached result if it exists and is not expired.

Parameters:

key (str)

Return type:

NLRBFetchResult | None

put(key, result)[source]

Write a result to the cache.

Parameters:
Return type:

None

invalidate(key)[source]

Remove a cache entry. Returns True if removed.

Parameters:

key (str)

Return type:

bool

invalidate_all()[source]

Remove all cache entries. Returns count removed.

Return type:

int

class siege_utilities.geo.providers.nlrb_cache.NLRBLoader[source]

Bases: object

Cache-aware loader: cache hit → fetch → cache write.

Parameters:
  • cache – NLRBCache instance. If None, creates default.

  • client – NLRBDataClient or similar with fetch_all() method.

__init__(cache=None, client=None)[source]
Parameters:

cache (NLRBCache | None)

load(key='unified', force_refresh=False)[source]

Load NLRB data, using cache when available.

Parameters:
  • key (str) – Cache key (e.g., ‘unified’, ‘labordata’, ‘datagov’).

  • force_refresh (bool) – Skip cache and fetch fresh data.

Return type:

NLRBFetchResult

NLRB data source clients for case, election, and ULP data.

Three backends:

  • data.gov — XML dumps of C-Cases (ULP) and R-Cases (representation).

  • labordata GitHub — cleaned CSV/Parquet exports from labordata/nlrb-data.

  • NxGen Advanced Search — HTML scraping of the NLRB case search (fragile; marked as best-effort).

A unified NLRBDataClient facade reconciles records across sources.

class siege_utilities.geo.providers.nlrb_clients.CaseStatus[source]

Bases: str, Enum

High-level case statuses.

OPEN = 'Open'
CLOSED = 'Closed'
PENDING = 'Pending'
UNKNOWN = 'Unknown'
__new__(value)
class siege_utilities.geo.providers.nlrb_clients.CaseType[source]

Bases: str, Enum

NLRB case type prefix codes.

R = 'R'
C = 'C'
CB = 'CB'
CD = 'CD'
CE = 'CE'
CA = 'CA'
AC = 'AC'
UC = 'UC'
UD = 'UD'
WH = 'WH'
RM = 'RM'
RD = 'RD'
RC = 'RC'
__new__(value)
class siege_utilities.geo.providers.nlrb_clients.ElectionRecord[source]

Bases: object

Election tally result linked to a case.

case_number: str
tally_date: date | None = None
eligible_voters: int | None = None
votes_for: int | None = None
votes_against: int | None = None
void_ballots: int | None = None
union_name: str = ''
source: str = ''
to_dict()[source]
Return type:

dict

__init__(case_number, tally_date=None, eligible_voters=None, votes_for=None, votes_against=None, void_ballots=None, union_name='', source='')
Parameters:
  • case_number (str)

  • tally_date (date | None)

  • eligible_voters (int | None)

  • votes_for (int | None)

  • votes_against (int | None)

  • void_ballots (int | None)

  • union_name (str)

  • source (str)

Return type:

None

class siege_utilities.geo.providers.nlrb_clients.NLRBCaseRecord[source]

Bases: object

A single NLRB case record from any data source.

case_number: str
case_type: str
case_name: str = ''
status: str = 'Unknown'
date_filed: date | None = None
date_closed: date | None = None
region: int | None = None
city: str = ''
state: str = ''
union_name: str = ''
unit_description: str = ''
naics_code: str = ''
employee_count: int | None = None
source: str = ''
to_dict()[source]
Return type:

dict

__init__(case_number, case_type, case_name='', status='Unknown', date_filed=None, date_closed=None, region=None, city='', state='', union_name='', unit_description='', naics_code='', employee_count=None, source='')
Parameters:
  • case_number (str)

  • case_type (str)

  • case_name (str)

  • status (str)

  • date_filed (date | None)

  • date_closed (date | None)

  • region (int | None)

  • city (str)

  • state (str)

  • union_name (str)

  • unit_description (str)

  • naics_code (str)

  • employee_count (int | None)

  • source (str)

Return type:

None

class siege_utilities.geo.providers.nlrb_clients.NLRBDataClient[source]

Bases: object

Unified NLRB data client that reconciles across multiple sources.

Fetches from available backends and deduplicates by case number. When the same case appears in multiple sources, labordata is preferred (cleaned data), then data.gov, then NxGen.

Parameters:
  • datagov_client – Optional pre-configured data.gov client.

  • labordata_client – Optional pre-configured labordata client.

  • nxgen_client – Optional pre-configured NxGen client.

  • use_nxgen – Whether to include the fragile NxGen source. Default False.

SOURCE_PRIORITY = ('labordata', 'datagov', 'nxgen')
__init__(datagov_client=None, labordata_client=None, nxgen_client=None, use_nxgen=False)[source]
Parameters:
fetch_all(datagov_params=None)[source]

Fetch from all configured sources and reconcile.

Parameters:

datagov_params (dict | None) – Optional query parameters for the data.gov source.

Returns:

Merged NLRBFetchResult with deduplicated records.

Return type:

NLRBFetchResult

class siege_utilities.geo.providers.nlrb_clients.NLRBDatagovClient[source]

Bases: object

Download and parse NLRB case data from data.gov XML exports.

The NLRB publishes case data through its open data portal. This client fetches the XML feed and parses it into structured records.

Parameters:
  • timeout – HTTP request timeout in seconds.

  • base_url – Override the default data.gov endpoint.

__init__(timeout=60, base_url=None, session=None)[source]
Parameters:
  • timeout (int)

  • base_url (str | None)

  • session (object | None)

fetch_cases(params=None)[source]

Fetch case records from the data.gov XML endpoint.

Parameters:

params (dict | None) – Optional query parameters for filtering.

Returns:

NLRBFetchResult with parsed case records.

Return type:

NLRBFetchResult

class siege_utilities.geo.providers.nlrb_clients.NLRBFetchResult[source]

Bases: object

Result from a client fetch operation.

cases: list[NLRBCaseRecord]
elections: list[ElectionRecord]
ulp_charges: list[ULPRecord]
errors: list[str]
source: str = ''
property success: bool
property total_records: int
__init__(cases=<factory>, elections=<factory>, ulp_charges=<factory>, errors=<factory>, source='')
Parameters:
Return type:

None

class siege_utilities.geo.providers.nlrb_clients.NLRBLabordataClient[source]

Bases: object

Download and parse NLRB data from the labordata/nlrb-data GitHub repo.

The labordata project maintains cleaned CSV exports of NLRB data. This client downloads the CSV files and parses them into structured records.

Parameters:
  • timeout – HTTP request timeout in seconds.

  • base_url – Override the default GitHub raw content URL.

__init__(timeout=120, base_url=None, session=None)[source]
Parameters:
  • timeout (int)

  • base_url (str | None)

  • session (object | None)

fetch_cases()[source]

Fetch all available data from the labordata repository.

Downloads cases, elections, and ULP charges CSVs.

Return type:

NLRBFetchResult

class siege_utilities.geo.providers.nlrb_clients.NLRBNxGenClient[source]

Bases: object

Scrape NLRB case data from the NxGen Advanced Search page.

Warning

This client relies on HTML structure that may change without notice. Use it as a fallback when data.gov or labordata sources are insufficient. Prefer the other clients for bulk data.

Parameters:
  • timeout – HTTP request timeout in seconds.

  • base_url – Override the default NxGen search URL.

__init__(timeout=30, base_url=None, session=None)[source]
Parameters:
  • timeout (int)

  • base_url (str | None)

  • session (object | None)

fetch_case(case_number)[source]

Fetch a single case by number from the NxGen search.

Parameters:

case_number (str) – NLRB case number (e.g., ‘05-RC-123456’).

Return type:

NLRBFetchResult

class siege_utilities.geo.providers.nlrb_clients.ULPRecord[source]

Bases: object

Unfair labor practice charge.

case_number: str
allegation: str = ''
charging_party: str = ''
charged_party: str = ''
section_of_act: str = ''
date_filed: date | None = None
source: str = ''
to_dict()[source]
Return type:

dict

__init__(case_number, allegation='', charging_party='', charged_party='', section_of_act='', date_filed=None, source='')
Parameters:
  • case_number (str)

  • allegation (str)

  • charging_party (str)

  • charged_party (str)

  • section_of_act (str)

  • date_filed (date | None)

  • source (str)

Return type:

None

DataFrame export for NLRB records.

Converts NLRBFetchResult or lists of dataclass records into pandas DataFrames for analysis.

siege_utilities.geo.providers.nlrb_export.cases_to_dataframe(cases)[source]

Convert a list of NLRBCaseRecord to a DataFrame.

Parameters:

cases – List of NLRBCaseRecord instances, or an NLRBFetchResult.

Returns:

pandas DataFrame with one row per case.

siege_utilities.geo.providers.nlrb_export.elections_to_dataframe(elections)[source]

Convert a list of ElectionRecord to a DataFrame.

siege_utilities.geo.providers.nlrb_export.fetch_result_to_dataframes(result)[source]

Convert an NLRBFetchResult to a dict of DataFrames.

Returns:

Dict with keys ‘cases’, ‘elections’, ‘ulp_charges’, each a DataFrame.

siege_utilities.geo.providers.nlrb_export.ulp_charges_to_dataframe(charges)[source]

Convert a list of ULPRecord to a DataFrame.

NLRB region lookup, case-to-region assignment, and aggregation.

Pure-Python utilities — no Django dependency. Works with NLRBCaseRecord dataclasses, dicts, or any object with region and state attributes.

siege_utilities.geo.providers.nlrb_regions.aggregate_by_region(records)[source]

Group records by their NLRB region number.

Parameters:

records – Iterable of case records (dataclass, model, or dict).

Returns:

Dict mapping region numbers to lists of records. Records without a region are grouped under key 0.

Return type:

dict[int, list]

siege_utilities.geo.providers.nlrb_regions.assign_region(case_record)[source]

Assign an NLRB region number to a case record.

Uses the region number from the case number if available, otherwise falls back to state-based lookup (first match).

Parameters:

case_record – Object with region and state attributes, or a dict with those keys.

Returns:

Region number or None if not determinable.

Return type:

int | None

siege_utilities.geo.providers.nlrb_regions.lookup_region_by_state(state)[source]

Return NLRB region number(s) for a state abbreviation.

Parameters:

state (str) – 2-letter state abbreviation (e.g., ‘IL’, ‘NY’).

Returns:

List of region numbers. May be >1 for states split across multiple regions (e.g., NY → [2, 3, 29]). Empty list if state not found.

Return type:

list[int]

siege_utilities.geo.providers.nlrb_regions.region_summary(records)[source]

Summarize case counts by region with office names.

Parameters:

records – Iterable of case records.

Returns:

region_number, office, case_count. Sorted by region_number.

Return type:

List of dicts with keys

Census Data Intelligence

Census API subpackage — split components of CensusAPIClient.

Each module handles one concern:

  • variable_registry — variable groups, descriptions, lookup, metadata

  • dataset_selector — pure-logic dataset/geography validation and URL building

  • api — HTTP transport, caching, rate limiting, response processing

  • catalog — hierarchical table metadata (Dataset → Subject → Family → Table → Variable)

  • catalog_populator — fetches metadata from Census API to populate the catalog

class siege_utilities.geo.census.CatalogCache[source]

Bases: object

Disk-backed JSON cache with TTL for CensusCatalog instances.

__init__(cache_dir=None, ttl_days=30)[source]
Parameters:
  • cache_dir (Path | None)

  • ttl_days (int)

get(dataset, year)[source]
Parameters:
Return type:

CensusCatalog | None

put(dataset, year, catalog)[source]
Parameters:
Return type:

None

clear(dataset='', year=0)[source]
Parameters:
Return type:

int

class siege_utilities.geo.census.CatalogLoader[source]

Bases: object

Cache-aware catalog loader.

Resolution order: cache → API fetch → cache write.

__init__(cache=None, base_url='')[source]
Parameters:
load(dataset, year, survey_type='', force_refresh=False)[source]
Parameters:
  • dataset (str)

  • year (int)

  • survey_type (str)

  • force_refresh (bool)

Return type:

CensusCatalog

class siege_utilities.geo.census.CensusAPI[source]

Bases: object

HTTP transport layer for Census Bureau API.

Responsibilities: API key resolution, request building, retry/rate-limit handling, caching (parquet or Django), and response normalization.

__init__(api_key=None, timeout=None, cache_dir=None, cache_backend='parquet', cache_ttl=86400)[source]
Parameters:
  • api_key (str | None)

  • timeout (int | None)

  • cache_dir (str | Path | None)

  • cache_backend (str)

  • cache_ttl (int)

fetch_data(variables, year, dataset='acs5', geography='county', state_fips=None, county_fips=None, include_moe=True)[source]

Fetch demographic data from the Census API.

Delegates variable resolution to VariableRegistry and geography validation to DatasetSelector, then handles the HTTP round-trip.

Parameters:
  • variables (str | List[str]) – Variable name(s) to fetch (resolved via VariableRegistry).

  • year (int) – Census data year.

  • dataset (str) – Census dataset identifier (e.g., ‘acs5’, ‘dec/sf1’).

  • geography (str) – Geographic level (e.g., ‘county’, ‘tract’, ‘zcta’).

  • state_fips (str | None) – State FIPS filter (required for sub-state geographies).

  • county_fips (str | None) – County FIPS filter.

  • include_moe (bool) – If True and dataset is ACS, append margin-of-error variables. Silently ignored for non-ACS datasets.

Return type:

pandas.DataFrame

clear_cache()[source]

Clear cached API responses from the parquet file cache.

Only clears the local parquet cache. Has no effect when cache_backend='django' (Django cache must be cleared via django.core.cache.cache.clear()).

Return type:

None

get_variable_metadata(variable, year, dataset='acs5')[source]
Parameters:
Return type:

Dict[str, Any]

list_available_variables(year, dataset='acs5', search=None)[source]
Parameters:
  • year (int)

  • dataset (str)

  • search (str | None)

Return type:

pandas.DataFrame

class siege_utilities.geo.census.CensusCatalog[source]

Bases: object

__init__()[source]
Return type:

None

add_table(table)[source]
Parameters:

table (CensusTable)

Return type:

None

add_tables(tables)[source]
Parameters:

tables (list[CensusTable])

Return type:

None

add_family(family)[source]
Parameters:

family (CensusFamily)

Return type:

None

add_subject(subject)[source]
Parameters:

subject (CensusSubject)

Return type:

None

add_dataset(dataset)[source]
Parameters:

dataset (CensusCatalogDataset)

Return type:

None

get_table(table_id)[source]
Parameters:

table_id (str)

Return type:

CensusTable | None

get_family(family_id)[source]
Parameters:

family_id (str)

Return type:

CensusFamily | None

get_subject(subject_id)[source]
Parameters:

subject_id (str)

Return type:

CensusSubject | None

get_dataset(dataset_id)[source]
Parameters:

dataset_id (str)

Return type:

CensusCatalogDataset | None

property tables: dict[str, CensusTable]
property families: dict[str, CensusFamily]
property subjects: dict[str, CensusSubject]
property datasets: dict[str, CensusCatalogDataset]
tables_for_geography(geo_level)[source]
Parameters:

geo_level (str)

Return type:

list[CensusTable]

geography_for_table(table_id)[source]

Return the geography levels supported by table_id.

Raises:

KeyError – If table_id is not in the catalog.

Parameters:

table_id (str)

Return type:

list[str]

families_for_table(table_id)[source]
Parameters:

table_id (str)

Return type:

list[CensusFamily]

search(query, level=None, dataset=None, max_results=25)[source]

Search the catalog across multiple levels.

Parameters:
  • query (str) – Free-text search query.

  • level (SearchLevel | None) – Restrict to a single search level, or None for all.

  • dataset (str | None) – Filter TABLE and VARIABLE results to tables belonging to this dataset. Has no effect on FAMILY, SUBJECT, or DATASET level results (those are cross-dataset).

  • max_results (int) – Maximum results to return.

Return type:

list[SearchResult]

build_families(max_topical_gap=100)[source]
Parameters:

max_topical_gap (int)

Return type:

None

class siege_utilities.geo.census.CensusCatalogPopulator[source]

Bases: object

__init__(base_url='https://api.census.gov/data', timeout=30)[source]
Parameters:
  • base_url (str)

  • timeout (int)

populate(dataset, year, survey_type='')[source]
Parameters:
Return type:

CensusCatalog

class siege_utilities.geo.census.CensusCatalogDataset[source]

Bases: object

CensusCatalogDataset(dataset_id: ‘str’, survey_type: ‘str’, year: ‘int’, subjects: ‘list[CensusSubject]’ = <factory>, tables: ‘dict[str, CensusTable]’ = <factory>)

dataset_id: str
survey_type: str
year: int
subjects: list[CensusSubject]
tables: dict[str, CensusTable]
__init__(dataset_id, survey_type, year, subjects=<factory>, tables=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.CensusFamily[source]

Bases: object

CensusFamily(family_id: ‘str’, family_type: ‘FamilyType’, root_table_id: ‘str’, tables: ‘list[CensusTable]’ = <factory>, description: ‘str’ = ‘’)

family_id: str
family_type: FamilyType
root_table_id: str
tables: list[CensusTable]
description: str = ''
property table_ids: list[str]
__init__(family_id, family_type, root_table_id, tables=<factory>, description='')
Parameters:
Return type:

None

class siege_utilities.geo.census.CensusSubject[source]

Bases: object

CensusSubject(subject_id: ‘str’, label: ‘str’, families: ‘list[CensusFamily]’ = <factory>, tables: ‘list[CensusTable]’ = <factory>)

subject_id: str
label: str
families: list[CensusFamily]
tables: list[CensusTable]
property all_tables: list[CensusTable]
__init__(subject_id, label, families=<factory>, tables=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.CensusTable[source]

Bases: object

CensusTable(table_id: ‘str’, label: ‘str’, concept: ‘str’, universe: ‘str’ = ‘’, variables: ‘list[CensusVariable]’ = <factory>, geography_levels: ‘list[str]’ = <factory>)

table_id: str
label: str
concept: str
universe: str = ''
variables: list[CensusVariable]
geography_levels: list[str]
property parsed_id: dict | None
property root_table_id: str | None
property race_suffix: str | None
__init__(table_id, label, concept, universe='', variables=<factory>, geography_levels=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.CensusVariable[source]

Bases: object

CensusVariable(code: ‘str’, label: ‘str’, concept: ‘str’, table_id: ‘str’, stat_type: ‘str’ = ‘E’)

code: str
label: str
concept: str
table_id: str
stat_type: str = 'E'
classmethod parse_code(code)[source]
Parameters:

code (str)

Return type:

dict | None

__init__(code, label, concept, table_id, stat_type='E')
Parameters:
Return type:

None

class siege_utilities.geo.census.DatasetSelector[source]

Bases: object

Handles dataset path resolution and geography validation/normalization for Census API queries. All methods are stateless class/static methods.

API_SUPPORTED_GEOGRAPHIES = frozenset({'block_group', 'county', 'place', 'state', 'tract', 'zcta'})
static get_dataset_path(year, dataset)[source]

Map a dataset identifier + year to the Census API URL path segment.

Parameters:
  • year (int) – Census year (e.g. 2020)

  • dataset (str) – One of ‘acs5’, ‘acs1’, ‘dec’, ‘pep’

Returns:

URL path segment, e.g. ‘2020/acs/acs5’

Raises:

ValueError – If dataset is unknown.

Return type:

str

classmethod validate_geography(geography, state_fips, county_fips)[source]

Validate and normalize a geography string to its canonical form.

Parameters:
  • geography (str) – Raw geography level (may be an alias like ‘bg’)

  • state_fips (str | None) – State FIPS code (required for tract/block_group)

  • county_fips (str | None) – County FIPS code

Returns:

Canonical geography string (e.g. ‘block_group’)

Raises:

ValueError – If geography is invalid or required FIPS codes are missing.

Return type:

str

static build_geography_clause(geography, state_fips, county_fips)[source]

Build the for=/in= geography clause for the Census API URL.

Parameters:
  • geography (str) – Canonical geography level (already validated)

  • state_fips (str | None) – State FIPS code

  • county_fips (str | None) – County FIPS code

Returns:

URL query-string fragment, e.g. for=county:*&in=state:06

Return type:

str

static normalize_state(state_input)[source]

Normalize a state name, abbreviation, or FIPS code to its FIPS code.

Raises:

ValueError – If the state cannot be resolved.

Parameters:

state_input (str)

Return type:

str

class siege_utilities.geo.census.FamilyType[source]

Bases: Enum

RACE_ITERATION = 'race_iteration'
TOPICAL_KINSHIP = 'topical_kinship'
class siege_utilities.geo.census.SearchLevel[source]

Bases: Enum

DATASET = 'dataset'
SUBJECT = 'subject'
FAMILY = 'family'
TABLE = 'table'
VARIABLE = 'variable'
class siege_utilities.geo.census.SearchResult[source]

Bases: object

SearchResult(level: ‘SearchLevel’, id: ‘str’, label: ‘str’, score: ‘float’, obj: ‘object’)

level: SearchLevel
id: str
label: str
score: float
obj: object
__init__(level, id, label, score, obj)
Parameters:
Return type:

None

class siege_utilities.geo.census.VariableRegistry[source]

Bases: object

Registry for Census variable groups, descriptions, and metadata lookup.

Pure data + logic — all methods are stateless except optional API calls for unknown variable metadata.

__init__(groups=None, descriptions=None)[source]
Parameters:
resolve_variables(variables)[source]

Resolve variable specification to list of variable codes.

Parameters:

variables (str | List[str])

Return type:

List[str]

add_moe_variables(variables)[source]

Add margin of error variables for ACS estimate variables.

Parameters:

variables (List[str])

Return type:

List[str]

get_description(variable)[source]

Get human-readable description for a variable code.

Parameters:

variable (str)

Return type:

str

get_variable_metadata(variable, year, dataset='acs5', base_url=None, timeout=30)[source]

Get metadata for a Census variable.

Checks local descriptions first, falls back to API if needed.

Parameters:
  • variable (str)

  • year (int)

  • dataset (str)

  • base_url (str | None)

  • timeout (int)

Return type:

Dict[str, Any]

list_available_variables(year, dataset='acs5', search=None, base_url=None, timeout=30)[source]

List available variables for a dataset from the Census API.

Parameters:
  • year (int)

  • dataset (str)

  • search (str | None)

  • base_url (str | None)

  • timeout (int)

Return type:

pandas.DataFrame

list_groups()[source]

Return available variable groups and their variable counts.

Return type:

Dict[str, int]

HTTP transport, caching, and response processing for Census API queries.

Handles all I/O: network requests, file/Django caching, rate limiting, retry logic, and DataFrame post-processing.

class siege_utilities.geo.census.api.CensusAPI[source]

Bases: object

HTTP transport layer for Census Bureau API.

Responsibilities: API key resolution, request building, retry/rate-limit handling, caching (parquet or Django), and response normalization.

__init__(api_key=None, timeout=None, cache_dir=None, cache_backend='parquet', cache_ttl=86400)[source]
Parameters:
  • api_key (str | None)

  • timeout (int | None)

  • cache_dir (str | Path | None)

  • cache_backend (str)

  • cache_ttl (int)

fetch_data(variables, year, dataset='acs5', geography='county', state_fips=None, county_fips=None, include_moe=True)[source]

Fetch demographic data from the Census API.

Delegates variable resolution to VariableRegistry and geography validation to DatasetSelector, then handles the HTTP round-trip.

Parameters:
  • variables (str | List[str]) – Variable name(s) to fetch (resolved via VariableRegistry).

  • year (int) – Census data year.

  • dataset (str) – Census dataset identifier (e.g., ‘acs5’, ‘dec/sf1’).

  • geography (str) – Geographic level (e.g., ‘county’, ‘tract’, ‘zcta’).

  • state_fips (str | None) – State FIPS filter (required for sub-state geographies).

  • county_fips (str | None) – County FIPS filter.

  • include_moe (bool) – If True and dataset is ACS, append margin-of-error variables. Silently ignored for non-ACS datasets.

Return type:

pandas.DataFrame

clear_cache()[source]

Clear cached API responses from the parquet file cache.

Only clears the local parquet cache. Has no effect when cache_backend='django' (Django cache must be cleared via django.core.cache.cache.clear()).

Return type:

None

get_variable_metadata(variable, year, dataset='acs5')[source]
Parameters:
Return type:

Dict[str, Any]

list_available_variables(year, dataset='acs5', search=None)[source]
Parameters:
  • year (int)

  • dataset (str)

  • search (str | None)

Return type:

pandas.DataFrame

Census table catalog — hierarchical metadata for Census tables and variables.

Hierarchy: Dataset → Subject → Family → Table → Variable

The key abstraction is Family, which the Census Bureau does not expose natively. Two family types exist:

  • Race iteration families: tables like B19001 / B19001A–I share a root table; the letter suffix signals the race/ethnicity iteration.

  • Topical kinship families: tables like B19001, B19013, B19025 share a subject-area prefix and related concepts (all income-related).

Both family types are first-class objects in the catalog.

class siege_utilities.geo.census.catalog.CensusCatalog[source]

Bases: object

__init__()[source]
Return type:

None

add_table(table)[source]
Parameters:

table (CensusTable)

Return type:

None

add_tables(tables)[source]
Parameters:

tables (list[CensusTable])

Return type:

None

add_family(family)[source]
Parameters:

family (CensusFamily)

Return type:

None

add_subject(subject)[source]
Parameters:

subject (CensusSubject)

Return type:

None

add_dataset(dataset)[source]
Parameters:

dataset (CensusCatalogDataset)

Return type:

None

get_table(table_id)[source]
Parameters:

table_id (str)

Return type:

CensusTable | None

get_family(family_id)[source]
Parameters:

family_id (str)

Return type:

CensusFamily | None

get_subject(subject_id)[source]
Parameters:

subject_id (str)

Return type:

CensusSubject | None

get_dataset(dataset_id)[source]
Parameters:

dataset_id (str)

Return type:

CensusCatalogDataset | None

property tables: dict[str, CensusTable]
property families: dict[str, CensusFamily]
property subjects: dict[str, CensusSubject]
property datasets: dict[str, CensusCatalogDataset]
tables_for_geography(geo_level)[source]
Parameters:

geo_level (str)

Return type:

list[CensusTable]

geography_for_table(table_id)[source]

Return the geography levels supported by table_id.

Raises:

KeyError – If table_id is not in the catalog.

Parameters:

table_id (str)

Return type:

list[str]

families_for_table(table_id)[source]
Parameters:

table_id (str)

Return type:

list[CensusFamily]

search(query, level=None, dataset=None, max_results=25)[source]

Search the catalog across multiple levels.

Parameters:
  • query (str) – Free-text search query.

  • level (SearchLevel | None) – Restrict to a single search level, or None for all.

  • dataset (str | None) – Filter TABLE and VARIABLE results to tables belonging to this dataset. Has no effect on FAMILY, SUBJECT, or DATASET level results (those are cross-dataset).

  • max_results (int) – Maximum results to return.

Return type:

list[SearchResult]

build_families(max_topical_gap=100)[source]
Parameters:

max_topical_gap (int)

Return type:

None

class siege_utilities.geo.census.catalog.CensusCatalogDataset[source]

Bases: object

CensusCatalogDataset(dataset_id: ‘str’, survey_type: ‘str’, year: ‘int’, subjects: ‘list[CensusSubject]’ = <factory>, tables: ‘dict[str, CensusTable]’ = <factory>)

dataset_id: str
survey_type: str
year: int
subjects: list[CensusSubject]
tables: dict[str, CensusTable]
__init__(dataset_id, survey_type, year, subjects=<factory>, tables=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.catalog.CensusFamily[source]

Bases: object

CensusFamily(family_id: ‘str’, family_type: ‘FamilyType’, root_table_id: ‘str’, tables: ‘list[CensusTable]’ = <factory>, description: ‘str’ = ‘’)

family_id: str
family_type: FamilyType
root_table_id: str
tables: list[CensusTable]
description: str = ''
property table_ids: list[str]
__init__(family_id, family_type, root_table_id, tables=<factory>, description='')
Parameters:
Return type:

None

class siege_utilities.geo.census.catalog.CensusSubject[source]

Bases: object

CensusSubject(subject_id: ‘str’, label: ‘str’, families: ‘list[CensusFamily]’ = <factory>, tables: ‘list[CensusTable]’ = <factory>)

subject_id: str
label: str
families: list[CensusFamily]
tables: list[CensusTable]
property all_tables: list[CensusTable]
__init__(subject_id, label, families=<factory>, tables=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.catalog.CensusTable[source]

Bases: object

CensusTable(table_id: ‘str’, label: ‘str’, concept: ‘str’, universe: ‘str’ = ‘’, variables: ‘list[CensusVariable]’ = <factory>, geography_levels: ‘list[str]’ = <factory>)

table_id: str
label: str
concept: str
universe: str = ''
variables: list[CensusVariable]
geography_levels: list[str]
property parsed_id: dict | None
property root_table_id: str | None
property race_suffix: str | None
__init__(table_id, label, concept, universe='', variables=<factory>, geography_levels=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.census.catalog.CensusVariable[source]

Bases: object

CensusVariable(code: ‘str’, label: ‘str’, concept: ‘str’, table_id: ‘str’, stat_type: ‘str’ = ‘E’)

code: str
label: str
concept: str
table_id: str
stat_type: str = 'E'
classmethod parse_code(code)[source]
Parameters:

code (str)

Return type:

dict | None

__init__(code, label, concept, table_id, stat_type='E')
Parameters:
Return type:

None

class siege_utilities.geo.census.catalog.FamilyType[source]

Bases: Enum

RACE_ITERATION = 'race_iteration'
TOPICAL_KINSHIP = 'topical_kinship'
class siege_utilities.geo.census.catalog.SearchLevel[source]

Bases: Enum

DATASET = 'dataset'
SUBJECT = 'subject'
FAMILY = 'family'
TABLE = 'table'
VARIABLE = 'variable'
class siege_utilities.geo.census.catalog.SearchResult[source]

Bases: object

SearchResult(level: ‘SearchLevel’, id: ‘str’, label: ‘str’, score: ‘float’, obj: ‘object’)

level: SearchLevel
id: str
label: str
score: float
obj: object
__init__(level, id, label, score, obj)
Parameters:
Return type:

None

siege_utilities.geo.census.catalog.detect_race_iteration_families(tables)[source]
Parameters:

tables (list[CensusTable])

Return type:

list[CensusFamily]

siege_utilities.geo.census.catalog.detect_topical_families(tables, max_number_gap=100)[source]
Parameters:
Return type:

list[CensusFamily]

siege_utilities.geo.census.catalog.parse_table_id(table_id)[source]
Parameters:

table_id (str)

Return type:

dict | None

Census catalog caching — disk persistence with TTL for CensusCatalog instances.

Stores serialized catalogs as JSON in ~/.siege_utilities/cache/census_catalog/. Follows the same cache-dir convention as CensusAPI and PLFileDownloader.

The loader orchestrates: cache hit → API fetch → cache write.

class siege_utilities.geo.census.catalog_cache.CatalogCache[source]

Bases: object

Disk-backed JSON cache with TTL for CensusCatalog instances.

__init__(cache_dir=None, ttl_days=30)[source]
Parameters:
  • cache_dir (Path | None)

  • ttl_days (int)

get(dataset, year)[source]
Parameters:
Return type:

CensusCatalog | None

put(dataset, year, catalog)[source]
Parameters:
Return type:

None

clear(dataset='', year=0)[source]
Parameters:
Return type:

int

class siege_utilities.geo.census.catalog_cache.CatalogLoader[source]

Bases: object

Cache-aware catalog loader.

Resolution order: cache → API fetch → cache write.

__init__(cache=None, base_url='')[source]
Parameters:
load(dataset, year, survey_type='', force_refresh=False)[source]
Parameters:
  • dataset (str)

  • year (int)

  • survey_type (str)

  • force_refresh (bool)

Return type:

CensusCatalog

Census catalog documentation generator.

Produces Markdown documentation from a CensusCatalog instance: - Index page with Subject → Family → Table tree - Per-table pages with variable list, geography availability, dataset membership

siege_utilities.geo.census.catalog_docs.generate_catalog_docs(catalog, output_dir, title='Census Table Catalog')[source]
Parameters:
Return type:

list[Path]

siege_utilities.geo.census.catalog_docs.generate_catalog_markdown(catalog, title='Census Table Catalog')[source]
Parameters:
Return type:

str

Census catalog populator — fetches metadata from Census API discovery endpoints.

Populates CensusCatalog instances from: - /data/{year}/{dataset}/variables.json — variable definitions - /data/{year}/{dataset}/groups.json — concept groupings

Separated from catalog.py to keep the data model I/O-free.

class siege_utilities.geo.census.catalog_populator.CensusCatalogPopulator[source]

Bases: object

__init__(base_url='https://api.census.gov/data', timeout=30)[source]
Parameters:
  • base_url (str)

  • timeout (int)

populate(dataset, year, survey_type='')[source]
Parameters:
Return type:

CensusCatalog

Dataset selection and geography validation for Census API queries.

Pure logic — no I/O, no network calls. Fully unit-testable without mocks.

class siege_utilities.geo.census.dataset_selector.DatasetSelector[source]

Bases: object

Handles dataset path resolution and geography validation/normalization for Census API queries. All methods are stateless class/static methods.

API_SUPPORTED_GEOGRAPHIES = frozenset({'block_group', 'county', 'place', 'state', 'tract', 'zcta'})
static get_dataset_path(year, dataset)[source]

Map a dataset identifier + year to the Census API URL path segment.

Parameters:
  • year (int) – Census year (e.g. 2020)

  • dataset (str) – One of ‘acs5’, ‘acs1’, ‘dec’, ‘pep’

Returns:

URL path segment, e.g. ‘2020/acs/acs5’

Raises:

ValueError – If dataset is unknown.

Return type:

str

classmethod validate_geography(geography, state_fips, county_fips)[source]

Validate and normalize a geography string to its canonical form.

Parameters:
  • geography (str) – Raw geography level (may be an alias like ‘bg’)

  • state_fips (str | None) – State FIPS code (required for tract/block_group)

  • county_fips (str | None) – County FIPS code

Returns:

Canonical geography string (e.g. ‘block_group’)

Raises:

ValueError – If geography is invalid or required FIPS codes are missing.

Return type:

str

static build_geography_clause(geography, state_fips, county_fips)[source]

Build the for=/in= geography clause for the Census API URL.

Parameters:
  • geography (str) – Canonical geography level (already validated)

  • state_fips (str | None) – State FIPS code

  • county_fips (str | None) – County FIPS code

Returns:

URL query-string fragment, e.g. for=county:*&in=state:06

Return type:

str

static normalize_state(state_input)[source]

Normalize a state name, abbreviation, or FIPS code to its FIPS code.

Raises:

ValueError – If the state cannot be resolved.

Parameters:

state_input (str)

Return type:

str

Last-fetched-vintage state tracking for Census TIGER pulls.

Supports cron-friendly “is there a new vintage published?” checks. The state file holds a single integer (the year most recently downloaded).

Usage:

from pathlib import Path
from siege_utilities.geo.census.tiger_state import check_for_updates

state = Path('/var/lib/sw/tiger.year')
has_update, latest = check_for_updates(state)
if has_update:
    # download via CensusTIGERProvider.get_boundary(...)
    set_last_fetched_vintage(state, latest)
siege_utilities.geo.census.tiger_state.get_last_fetched_vintage(state_file)[source]

Read the last-fetched vintage year from state_file.

Returns None when the file is absent or unparseable.

Parameters:

state_file (Path)

Return type:

int | None

siege_utilities.geo.census.tiger_state.set_last_fetched_vintage(state_file, year)[source]

Write year to state_file. Creates parent dirs if needed.

Parameters:
Return type:

None

siege_utilities.geo.census.tiger_state.check_for_updates(state_file, *, provider=None)[source]

Return (has_update, latest_year).

has_update is True when the latest published vintage is greater than the recorded last-fetched year, OR when no state file exists. Returns (False, None) when discovery fails (offline / endpoint down).

provider defaults to a fresh CensusTIGERProvider; pass a pre-built one to share session state or to inject a test double.

Parameters:

state_file (Path)

Return type:

tuple[bool, int | None]

Variable registry for Census API variable groups, descriptions, and metadata.

Pure data + lookup logic — no I/O except optional API calls for unknown variables.

class siege_utilities.geo.census.variable_registry.VariableRegistry[source]

Bases: object

Registry for Census variable groups, descriptions, and metadata lookup.

Pure data + logic — all methods are stateless except optional API calls for unknown variable metadata.

__init__(groups=None, descriptions=None)[source]
Parameters:
resolve_variables(variables)[source]

Resolve variable specification to list of variable codes.

Parameters:

variables (str | List[str])

Return type:

List[str]

add_moe_variables(variables)[source]

Add margin of error variables for ACS estimate variables.

Parameters:

variables (List[str])

Return type:

List[str]

get_description(variable)[source]

Get human-readable description for a variable code.

Parameters:

variable (str)

Return type:

str

get_variable_metadata(variable, year, dataset='acs5', base_url=None, timeout=30)[source]

Get metadata for a Census variable.

Checks local descriptions first, falls back to API if needed.

Parameters:
  • variable (str)

  • year (int)

  • dataset (str)

  • base_url (str | None)

  • timeout (int)

Return type:

Dict[str, Any]

list_available_variables(year, dataset='acs5', search=None, base_url=None, timeout=30)[source]

List available variables for a dataset from the Census API.

Parameters:
  • year (int)

  • dataset (str)

  • search (str | None)

  • base_url (str | None)

  • timeout (int)

Return type:

pandas.DataFrame

list_groups()[source]

Return available variable groups and their variable counts.

Return type:

Dict[str, int]

Census API Client for fetching demographic data from the Census Bureau.

This module provides a client for fetching demographic data (population, income, education, etc.) from the Census Bureau API. The data can be joined with existing TIGER/Line geometries for spatial analysis.

Example usage:

from siege_utilities.geo import CensusAPIClient, get_demographics

# Convenience function df = get_demographics(state=’California’, geography=’county’, year=2020)

# Direct client usage client = CensusAPIClient() df = client.fetch_data(

variables=’income’, year=2020, dataset=’acs5’, geography=’tract’, state_fips=’06’, county_fips=’037’, include_moe=True

)

Implementation is split across three focused modules under census/:

  • census.variable_registry — variable groups, descriptions, metadata

  • census.dataset_selector — pure-logic geography/dataset validation

  • census.api — HTTP transport, caching, retry, response processing

This file provides the backward-compatible CensusAPIClient facade.

exception siege_utilities.geo.census_api_client.CensusAPIError[source]

Bases: Exception

Base exception for Census API errors.

exception siege_utilities.geo.census_api_client.CensusAPIKeyError[source]

Bases: CensusAPIError

Error related to API key authentication.

exception siege_utilities.geo.census_api_client.CensusRateLimitError[source]

Bases: CensusAPIError

Rate limit exceeded error.

exception siege_utilities.geo.census_api_client.CensusVariableError[source]

Bases: CensusAPIError

Invalid or unavailable variable error.

exception siege_utilities.geo.census_api_client.CensusGeographyError[source]

Bases: CensusAPIError

Invalid geography specification error.

class siege_utilities.geo.census_api_client.CensusAPIClient[source]

Bases: object

Client for fetching demographic data from the Census Bureau API.

This is a backward-compatible facade that delegates to:

  • CensusAPI for HTTP, caching, and retry logic

  • DatasetSelector for geography/dataset validation

  • VariableRegistry for variable resolution and metadata

All existing method signatures are preserved.

__init__(api_key=None, timeout=None, cache_dir=None, cache_backend='parquet', cache_ttl=86400)[source]
Parameters:
  • api_key (str | None)

  • timeout (int | None)

  • cache_dir (str | Path | None)

  • cache_backend (str)

  • cache_ttl (int)

property api_key
property timeout
property base_url
property cache_dir
property cache_backend
property cache_ttl
fetch_data(variables, year, dataset='acs5', geography='county', state_fips=None, county_fips=None, include_moe=True)[source]

Fetch demographic data from the Census API.

Parameters:
Return type:

pandas.DataFrame

get_variable_metadata(variable, year, dataset='acs5')[source]

Get metadata for a Census variable.

Parameters:
Return type:

Dict[str, Any]

list_available_variables(year, dataset='acs5', search=None)[source]

List available variables for a dataset.

Parameters:
  • year (int)

  • dataset (str)

  • search (str | None)

Return type:

pandas.DataFrame

clear_cache()[source]

Clear all cached API responses.

Return type:

None

siege_utilities.geo.census_api_client.get_demographics(state, geography='county', year=None, variables='demographics_basic', include_moe=True, dataset='acs5')[source]

Convenience function to get demographic data.

Parameters:
  • state (str) – State name, abbreviation, or FIPS code

  • geography (str) – Geographic level (‘county’, ‘tract’, ‘block_group’)

  • year (int | None) – Census year (defaults to most recent available)

  • variables (str) – Variable group name or list of variables

  • include_moe (bool) – Include margin of error columns

  • dataset (str) – Census dataset (‘acs5’, ‘acs1’, ‘dec’)

Returns:

DataFrame with demographic data

Return type:

pandas.DataFrame

siege_utilities.geo.census_api_client.get_population(state, geography='tract', year=None, dataset='acs5')[source]

Convenience function to get population data.

Parameters:
  • state (str)

  • geography (str)

  • year (int | None)

  • dataset (str)

Return type:

pandas.DataFrame

siege_utilities.geo.census_api_client.get_income_data(state, geography='tract', year=None, include_moe=True, dataset='acs5')[source]

Convenience function to get income data.

Parameters:
  • state (str)

  • geography (str)

  • year (int | None)

  • include_moe (bool)

  • dataset (str)

Return type:

pandas.DataFrame

siege_utilities.geo.census_api_client.get_education_data(state, geography='tract', year=None, include_moe=True, dataset='acs5')[source]

Convenience function to get educational attainment data.

Parameters:
  • state (str)

  • geography (str)

  • year (int | None)

  • include_moe (bool)

  • dataset (str)

Return type:

pandas.DataFrame

siege_utilities.geo.census_api_client.get_housing_data(state, geography='tract', year=None, include_moe=True, dataset='acs5')[source]

Convenience function to get housing data.

Parameters:
  • state (str)

  • geography (str)

  • year (int | None)

  • include_moe (bool)

  • dataset (str)

Return type:

pandas.DataFrame

siege_utilities.geo.census_api_client.get_census_api_client()[source]

Get or create the default Census API client.

Return type:

CensusAPIClient

siege_utilities.geo.census_api_client.get_census_data_with_geometry(year, geography, variables, state=None, county_fips=None, dataset='acs5', include_moe=True)[source]

Fetch Census boundaries AND demographic data, returning a merged GeoDataFrame.

This is the primary function for getting choropleth-ready data. It: 1. Fetches TIGER/Line geometry for the specified geography 2. Fetches demographic variables from the Census API 3. Joins them on GEOID

Parameters:
Return type:

gpd.GeoDataFrame

siege_utilities.geo.census_api_client.join_demographics_to_shapes(shapes_gdf, demographics_df, shapes_geoid_col=None, demographics_geoid_col='GEOID', how='left')[source]

Join demographic data to existing shapes GeoDataFrame.

Use this when you already have shapes loaded and want to add demographic data.

Parameters:
  • shapes_gdf (gpd.GeoDataFrame)

  • demographics_df (pandas.DataFrame)

  • shapes_geoid_col (str | None)

  • demographics_geoid_col (str)

  • how (str)

Return type:

gpd.GeoDataFrame

Intelligent Census Data Selector

This module provides intelligent functions for automatically selecting the best Census datasets based on analysis requirements, geography needs, and time sensitivity.

class siege_utilities.geo.census_data_selector.CensusDataSelector[source]

Bases: object

Intelligent selector for Census data based on analysis requirements.

This class automatically determines the best Census datasets to use for different types of analysis, considering factors like: - Geography level requirements - Time sensitivity - Data reliability needs - Variable requirements - Analysis purpose

__init__()[source]
select_datasets_for_analysis(analysis_type, geography_level, time_period=None, variables=None, reliability_requirement=None)[source]

Select the best Census datasets for a specific analysis.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “housing”, “business”)

  • geography_level (str | GeographyLevel) – Required geography level

  • time_period (str | None) – Specific time period (optional)

  • variables (List[str] | None) – Required variables (optional)

  • reliability_requirement (str | None) – Reliability requirement (“low”, “medium”, “high”)

Returns:

Dictionary with dataset recommendations and rationale. Suitability scores use 0-5 scale: 0-2.0 (poor), 2.1-3.0 (moderate), 3.1-4.0 (good), 4.1-5.0 (excellent match)

Return type:

Dict[str, Any]

get_dataset_compatibility_matrix(analysis_types=None)[source]

Generate a compatibility matrix showing which datasets work best for different analysis types.

Parameters:

analysis_types (List[str] | None) – List of analysis types to include (default: all)

Returns:

DataFrame with compatibility scores

Return type:

pandas.DataFrame

suggest_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Suggest the best approach for conducting a specific analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str | GeographyLevel) – Required geography level

  • time_constraints (str | None) – Time constraints (“quick”, “standard”, “comprehensive”)

Returns:

Dictionary with analysis approach recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.census_data_selector.get_census_data_selector()[source]

Get a configured Census data selector instance.

Return type:

CensusDataSelector

siege_utilities.geo.census_data_selector.select_census_datasets(analysis_type, geography_level, time_period=None, variables=None)[source]

Convenience function to select Census datasets for analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_period (str | None) – Specific time period (optional)

  • variables (List[str] | None) – Required variables (optional)

Returns:

Dataset recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.census_data_selector.select_datasets_for_analysis(analysis_type, geography_level, time_period=None, variables=None)[source]

Standalone function to select datasets for analysis.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “housing”, “business”)

  • geography_level (str) – Required geography level (e.g., “tract”, “county”, “state”)

  • time_period (str | None) – Time period preference (e.g., “latest”, “comprehensive”)

  • variables (List[str] | None) – List of required variables

Returns:

Dataset recommendations and rationale

Return type:

Dict[str, Any]

siege_utilities.geo.census_data_selector.get_dataset_compatibility_matrix(analysis_types=None)[source]

Standalone function to get dataset compatibility matrix.

Parameters:

analysis_types (List[str] | None) – List of analysis types to include (optional)

Returns:

DataFrame with compatibility scores

Return type:

pandas.DataFrame

siege_utilities.geo.census_data_selector.get_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Convenience function to get analysis approach recommendations.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_constraints (str | None) – Time constraints (optional)

Returns:

Analysis approach recommendations

Return type:

Dict[str, Any]

siege_utilities.geo.census_data_selector.suggest_analysis_approach(analysis_type, geography_level, time_constraints=None)[source]

Standalone function to suggest analysis approach.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str | GeographyLevel) – Required geography level

  • time_constraints (str | None) – Time constraints (“quick”, “standard”, “comprehensive”)

Returns:

Dictionary with analysis approach recommendations

Return type:

Dict[str, Any]

Census Dataset Relationship Mapper

This module provides comprehensive mapping and relationship information for Census datasets, making it easier to understand which datasets to use for different analysis needs.

class siege_utilities.geo.census_dataset_mapper.SurveyType[source]

Bases: Enum

Enumeration of Census survey types.

DECENNIAL = 'decennial'
ACS_1YR = 'acs_1yr'
ACS_3YR = 'acs_3yr'
ACS_5YR = 'acs_5yr'
CENSUS_BUSINESS = 'census_business'
POPULATION_ESTIMATES = 'population_estimates'
HOUSING_ESTIMATES = 'housing_estimates'
class siege_utilities.geo.census_dataset_mapper.GeographyLevel[source]

Bases: Enum

Enumeration of Census geography levels (canonical names).

NATION = 'nation'
REGION = 'region'
DIVISION = 'division'
STATE = 'state'
COUNTY = 'county'
COUSUB = 'cousub'
PLACE = 'place'
CD = 'cd'
SLDU = 'sldu'
SLDL = 'sldl'
TRACT = 'tract'
BLOCK_GROUP = 'block_group'
BLOCK = 'block'
ZCTA = 'zcta'
CBSA = 'cbsa'
PUMA = 'puma'
VTD = 'vtd'
VTD20 = 'vtd20'
TABBLOCK20 = 'tabblock20'
TABBLOCK10 = 'tabblock10'
COUNTY_SUBDIVISION = 'cousub'
CONGRESSIONAL_DISTRICT = 'cd'
STATE_LEGISLATIVE_DISTRICT = 'sldu'
ZIP_CODE = 'zcta'
VOTING_DISTRICT = 'vtd'
class siege_utilities.geo.census_dataset_mapper.DataReliability[source]

Bases: Enum

Enumeration of data reliability levels.

HIGH = 'high'
MEDIUM = 'medium'
LOW = 'low'
ESTIMATED = 'estimated'
class siege_utilities.geo.census_dataset_mapper.CensusDataset[source]

Bases: object

Represents a Census dataset with metadata.

dataset_id: str
name: str
survey_type: SurveyType
geography_levels: List[GeographyLevel]
time_period: str
reliability: DataReliability
description: str
variables: List[str]
limitations: List[str]
best_for: List[str]
alternatives: List[str]
data_quality_notes: str
last_updated: str
next_update: str
api_endpoint: str | None = None
download_url: str | None = None
__init__(dataset_id, name, survey_type, geography_levels, time_period, reliability, description, variables, limitations, best_for, alternatives, data_quality_notes, last_updated, next_update, api_endpoint=None, download_url=None)
Parameters:
Return type:

None

class siege_utilities.geo.census_dataset_mapper.DatasetRelationship[source]

Bases: object

Represents a relationship between Census datasets.

primary_dataset: str
related_dataset: str
relationship_type: str
description: str
when_to_use_primary: str
overlap_period: str | None = None
compatibility_score: float = 1.0
__init__(primary_dataset, related_dataset, relationship_type, description, when_to_use_primary, when_to_use_related, overlap_period=None, compatibility_score=1.0)
Parameters:
  • primary_dataset (str)

  • related_dataset (str)

  • relationship_type (str)

  • description (str)

  • when_to_use_primary (str)

  • when_to_use_related (str)

  • overlap_period (str | None)

  • compatibility_score (float)

Return type:

None

class siege_utilities.geo.census_dataset_mapper.CensusDatasetMapper[source]

Bases: object

Maps relationships between Census datasets and provides intelligent data selection.

This class helps users understand: - Which datasets are available for different geography levels - How different survey types relate to each other - When to use different datasets based on analysis needs - Data quality and reliability considerations

__init__()[source]
datasets: Dict[str, CensusDataset]
relationships: List[DatasetRelationship]
get_dataset_info(dataset_name)[source]

Get information about a specific dataset.

Parameters:

dataset_name (str)

Return type:

CensusDataset | None

list_datasets_by_type(survey_type)[source]

List all datasets of a specific survey type.

Parameters:

survey_type (SurveyType)

Return type:

List[CensusDataset]

list_datasets_by_geography(geography_level)[source]

List all datasets available for a specific geography level.

Parameters:

geography_level (GeographyLevel)

Return type:

List[CensusDataset]

get_best_dataset_for_use_case(use_case, geography_level, time_period=None)[source]

Get the best dataset(s) for a specific use case.

Parameters:
  • use_case (str) – Description of what you’re trying to analyze

  • geography_level (GeographyLevel) – Required geography level

  • time_period (str | None) – Specific time period (optional)

Returns:

List of recommended datasets, ranked by suitability

Return type:

List[CensusDataset]

get_dataset_relationships(dataset_name)[source]

Get all relationships for a specific dataset.

Parameters:

dataset_name (str)

Return type:

List[DatasetRelationship]

compare_datasets(dataset1_name, dataset2_name)[source]

Compare two datasets side by side.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

get_data_selection_guide(analysis_type, geography_level, time_sensitivity='medium')[source]

Get a comprehensive guide for selecting Census data.

Parameters:
  • analysis_type (str) – Type of analysis (e.g., “demographics”, “business”, “housing”)

  • geography_level (GeographyLevel) – Required geography level

  • time_sensitivity (str) – How time-sensitive the analysis is (“low”, “medium”, “high”)

Returns:

Dictionary with data selection recommendations

Return type:

Dict[str, Any]

export_dataset_catalog(filepath)[source]

Export the complete dataset catalog to JSON.

Parameters:

filepath (str)

siege_utilities.geo.census_dataset_mapper.get_census_dataset_mapper()[source]

Get a configured Census dataset mapper instance.

Return type:

CensusDatasetMapper

siege_utilities.geo.census_dataset_mapper.get_best_dataset_for_analysis(analysis_type, geography_level, time_sensitivity='medium')[source]

Convenience function to get the best dataset for analysis.

Parameters:
  • analysis_type (str) – Type of analysis needed

  • geography_level (str) – Required geography level

  • time_sensitivity (str) – Time sensitivity level

Returns:

Data selection guide

Return type:

Dict[str, Any]

siege_utilities.geo.census_dataset_mapper.get_dataset_info(dataset_name)[source]

Convenience function to get information about a specific dataset.

Parameters:

dataset_name (str)

Return type:

CensusDataset | None

siege_utilities.geo.census_dataset_mapper.list_datasets_by_type(survey_type)[source]

Convenience function to list all datasets of a specific survey type.

Parameters:

survey_type (str)

Return type:

List[CensusDataset]

siege_utilities.geo.census_dataset_mapper.list_datasets_by_geography(geography_level)[source]

Convenience function to list all datasets for a specific geography level.

Parameters:

geography_level (str)

Return type:

List[CensusDataset]

siege_utilities.geo.census_dataset_mapper.get_dataset_relationships(dataset_name)[source]

Convenience function to get relationships for a dataset.

Parameters:

dataset_name (str)

Return type:

List[DatasetRelationship]

siege_utilities.geo.census_dataset_mapper.get_best_dataset_for_use_case(use_case, geography_level='county', time_sensitivity='medium')[source]

Convenience function to get the best dataset for a specific use case.

Parameters:
  • use_case (str)

  • geography_level (str)

  • time_sensitivity (str)

Return type:

Dict[str, Any]

siege_utilities.geo.census_dataset_mapper.export_dataset_catalog(filepath)[source]

Convenience function to export the dataset catalog.

Parameters:

filepath (str)

siege_utilities.geo.census_dataset_mapper.get_data_selection_guide(analysis_type, geography_level, time_sensitivity='medium')[source]

Convenience function to get data selection guide.

Parameters:
  • analysis_type (str)

  • geography_level (str)

  • time_sensitivity (str)

Return type:

Dict[str, Any]

siege_utilities.geo.census_dataset_mapper.compare_datasets(dataset1_name, dataset2_name)[source]

Convenience function to compare two datasets.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

siege_utilities.geo.census_dataset_mapper.compare_census_datasets(dataset1_name, dataset2_name)[source]

Convenience function to compare two Census datasets.

Parameters:
  • dataset1_name (str)

  • dataset2_name (str)

Return type:

Dict[str, Any]

Deprecation shim — re-exports from siege_utilities.geo.providers.census_geocoder.

Moved during ELE-2438. Will be removed in v4.0.0.

exception siege_utilities.geo.census_geocoder.CensusGeocodeError[source]

Bases: RuntimeError

Raised when the Census geocoder API call fails unexpectedly.

Distinct from “no match” results (which return a CensusGeocodeResult with matched=False). This exception indicates an API / network / parse failure where the geocoder could not even attempt to match. Use __cause__ to inspect the underlying exception.

class siege_utilities.geo.census_geocoder.CensusGeocodeResult[source]

Bases: object

Result from Census Bureau geocoding.

matched

Whether the address was matched.

Type:

bool

input_address

The original input address string.

Type:

str

matched_address

The standardized matched address (if matched).

Type:

str

lat

Latitude (if matched).

Type:

float | None

lon

Longitude (if matched).

Type:

float | None

state_fips

2-digit state FIPS code.

Type:

str

county_fips

3-digit county FIPS code.

Type:

str

tract

6-digit tract code.

Type:

str

block

4-digit block code.

Type:

str

match_type

“Exact”, “Non_Exact”, or “No_Match”.

Type:

str

side

Street side (“L” or “R”, from TIGER).

Type:

str

tiger_line_id

TIGER/Line feature ID.

Type:

str

matched: bool = False
input_address: str = ''
matched_address: str = ''
lat: float | None = None
lon: float | None = None
state_fips: str = ''
county_fips: str = ''
tract: str = ''
block: str = ''
match_type: str = 'No_Match'
side: str = ''
tiger_line_id: str = ''
input_id: str = ''
property state_geoid: str

2-digit state GEOID.

property county_geoid: str

5-digit county GEOID (state + county FIPS).

property tract_geoid: str

11-digit tract GEOID (state + county + tract).

property block_geoid: str

15-digit block GEOID (state + county + tract + block).

property block_group_geoid: str

12-digit block group GEOID (block GEOID truncated to 12 chars).

__init__(matched=False, input_address='', matched_address='', lat=None, lon=None, state_fips='', county_fips='', tract='', block='', match_type='No_Match', side='', tiger_line_id='', input_id='')
Parameters:
Return type:

None

class siege_utilities.geo.census_geocoder.CensusVintage[source]

Bases: str, Enum

Census geocoder benchmark/vintage pairs.

Each member’s value encodes <benchmark>|<vintage> where both halves are the exact strings the Census Geocoder API expects. Use the .benchmark and .vintage properties to extract them.

The benchmark names follow the Public_AR_<label> pattern used by https://geocoding.geo.census.gov/geocoder/benchmarks. The vintage names follow the <label>_<benchmark-label> pattern returned by https://geocoding.geo.census.gov/geocoder/vintages?benchmark=….

CENSUS_2010 = 'Public_AR_Census2020|Census2010_Census2020'
CENSUS_2020 = 'Public_AR_Census2020|Census2020_Census2020'
CURRENT = 'Public_AR_Current|Current_Current'
ACS_2022 = 'Public_AR_Current|ACS2022_Current'
ACS_2023 = 'Public_AR_Current|ACS2023_Current'
property benchmark: str

The benchmark string accepted by the Census Geocoder API.

property vintage: str

The vintage string accepted by the Census Geocoder API.

__new__(value)
siege_utilities.geo.census_geocoder.geocode_batch(addresses, vintage=CensusVintage.CURRENT)[source]

Geocode a batch of addresses via the Census Bureau batch API.

The Census batch API accepts up to 10,000 addresses per request. Each address dict should have keys: id, street, city, state, zipcode.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

List of CensusGeocodeResult, one per input address (order preserved).

Raises:

ValueError – If batch exceeds 10,000 addresses.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.census_geocoder.geocode_batch_chunked(addresses, vintage=CensusVintage.CURRENT, chunk_size=10000)[source]

Geocode addresses in chunks of up to chunk_size (default 10,000).

Convenience wrapper around geocode_batch() for larger datasets.

Parameters:
  • addresses (list[dict]) – List of dicts with keys {id, street, city, state, zipcode}.

  • vintage (CensusVintage) – Census vintage for boundary matching.

  • chunk_size (int) – Max addresses per API call (max 10,000).

Returns:

List of CensusGeocodeResult, one per input address.

Return type:

list[CensusGeocodeResult]

siege_utilities.geo.census_geocoder.geocode_results_to_dataframe(results)[source]

Convert a list of CensusGeocodeResult to a pandas DataFrame.

Extracts all dataclass fields and computed GEOID properties into a flat DataFrame with standard column names. This eliminates the per-field list-comprehension boilerplate that every caller of geocode_batch() otherwise has to write.

Parameters:

results (list[CensusGeocodeResult]) – List of CensusGeocodeResult objects, typically returned by geocode_batch() or geocode_batch_chunked().

Returns:

input_id, input_address, matched, match_type, matched_address, latitude, longitude, state_geoid, county_geoid, tract_geoid, block_geoid, block_group_geoid, state_fips, county_fips, tract, block, side, tiger_line_id. For unmatched rows, latitude/longitude are None and GEOID columns are empty strings.

Return type:

DataFrame with columns

Raises:

TypeError – If results is not a list.

Example:

results = geocode_batch(addresses)
df = geocode_results_to_dataframe(results)
# df now has tract_geoid, block_geoid, lat/lon, etc.
siege_utilities.geo.census_geocoder.geocode_single(street, city, state, zipcode, vintage=CensusVintage.CURRENT)[source]

Geocode a single address via the Census Bureau API.

Parameters:
  • street (str) – Street address (e.g., “1600 Pennsylvania Ave NW”).

  • city (str) – City name.

  • state (str) – State abbreviation or name.

  • zipcode (str) – ZIP code.

  • vintage (CensusVintage) – Census vintage for boundary matching.

Returns:

CensusGeocodeResult with lat/lon and FIPS codes if matched.

Return type:

CensusGeocodeResult

siege_utilities.geo.census_geocoder.select_vintage_for_cycle(year)[source]

Select the appropriate Census vintage for an FEC cycle year.

Parameters:

year (int) – The FEC election cycle year.

Returns:

CensusVintage matching the boundaries in effect for that cycle.

Return type:

CensusVintage

Examples

>>> select_vintage_for_cycle(2024)
<CensusVintage.CURRENT: 'Public_AR_Current|Current_Current'>
>>> select_vintage_for_cycle(2016)
<CensusVintage.CENSUS_2020: 'Public_AR_Census2020|Census2020_Census2020'>
>>> select_vintage_for_cycle(2008)
<CensusVintage.CENSUS_2010: 'Public_AR_Census2020|Census2010_Census2020'>

Census file download and processing utilities.

This module provides tools for downloading Census data files directly, including PL 94-171 redistricting files that are not available via API.

Key features: - Download PL 94-171 files for block-level redistricting data - Download DHC (Demographic and Housing Characteristics) files - Download TIGER/Line shapefiles - Automatic caching and state filtering

class siege_utilities.geo.census_files.PLFileDownloader[source]

Bases: object

Downloader for PL 94-171 redistricting data files.

This class handles downloading, caching, and parsing PL files from the Census Bureau.

__init__(cache_dir=None, timeout=300)[source]

Initialize the PL file downloader.

Parameters:
  • cache_dir (str | Path | None) – Directory for caching files

  • timeout (int) – Request timeout in seconds

get_data(state, year=2020, geography='tract', county=None, tables=None)[source]

Get PL 94-171 data for a state.

Parameters:
  • state (str) – State name, abbreviation, or FIPS code

  • year (int) – Census year (2010 or 2020)

  • geography (str) – Geographic level (‘state’, ‘county’, ‘tract’, ‘block_group’, ‘block’)

  • county (str | None) – Optional county FIPS code to filter to

  • tables (List[str] | None) – List of tables to include (e.g., [‘P1’, ‘P2’, ‘H1’]). If None, includes all tables.

Returns:

DataFrame with PL data at specified geography level

Return type:

pandas.DataFrame

clear_cache()[source]

Clear all cached PL files.

Return type:

int

siege_utilities.geo.census_files.get_pl_data(state, year=2020, geography='tract', county=None, tables=None)[source]

Get PL 94-171 redistricting data.

Parameters:
  • state (str) – State name, abbreviation, or FIPS code

  • year (int) – Census year (2010 or 2020)

  • geography (str) – Geographic level

  • county (str | None) – Optional county FIPS filter

  • tables (List[str] | None) – Tables to include (default: all)

Returns:

DataFrame with PL data

Return type:

pandas.DataFrame

Example

# Get California tract data df = get_pl_data(state=’CA’, geography=’tract’)

# Get LA County block data df = get_pl_data(state=’CA’, geography=’block’, county=’037’)

siege_utilities.geo.census_files.get_pl_blocks(state, county=None, year=2020, tables=None)[source]

Get block-level PL 94-171 data.

This is the primary function for redistricting analysis as it provides the finest geographic detail available.

Parameters:
  • state (str) – State identifier

  • county (str | None) – Optional county FIPS to filter

  • year (int) – Census year

  • tables (List[str] | None) – Tables to include

Returns:

DataFrame with block-level PL data

Return type:

pandas.DataFrame

siege_utilities.geo.census_files.get_pl_tracts(state, county=None, year=2020, tables=None)[source]

Get tract-level PL 94-171 data.

Parameters:
  • state (str) – State identifier

  • county (str | None) – Optional county FIPS filter

  • year (int) – Census year

  • tables (List[str] | None) – Tables to include

Returns:

DataFrame with tract-level PL data

Return type:

pandas.DataFrame

siege_utilities.geo.census_files.download_pl_file(state, year=2020, output_dir=None)[source]

Download raw PL 94-171 zip file.

Parameters:
  • state (str) – State identifier

  • year (int) – Census year

  • output_dir (str | Path | None) – Directory to save file

Returns:

Path to downloaded file

Return type:

Path

siege_utilities.geo.census_files.list_available_pl_files(year=2020)[source]

List available PL 94-171 files.

Parameters:

year (int) – Census year

Returns:

List of available states with file info

Return type:

List[Dict]

PL 94-171 Redistricting Data File Downloader.

This module downloads and processes PL 94-171 redistricting data files from the Census Bureau. Unlike the Census API, these files provide block-level data essential for redistricting.

PL 94-171 File Structure: - Files are distributed as pipe-delimited text files - Each state has separate files for geographic header and data segments - Data is organized by summary level (state, county, tract, block group, block)

Example usage:

from siege_utilities.geo.census_files import get_pl_data, get_pl_blocks

# Get all PL data for California at tract level df = get_pl_data(state=’CA’, geography=’tract’, year=2020)

# Get block-level data for LA County blocks = get_pl_blocks(state=’CA’, county=’037’, year=2020)

class siege_utilities.geo.census_files.pl_downloader.PLFileDownloader[source]

Bases: object

Downloader for PL 94-171 redistricting data files.

This class handles downloading, caching, and parsing PL files from the Census Bureau.

__init__(cache_dir=None, timeout=300)[source]

Initialize the PL file downloader.

Parameters:
  • cache_dir (str | Path | None) – Directory for caching files

  • timeout (int) – Request timeout in seconds

get_data(state, year=2020, geography='tract', county=None, tables=None)[source]

Get PL 94-171 data for a state.

Parameters:
  • state (str) – State name, abbreviation, or FIPS code

  • year (int) – Census year (2010 or 2020)

  • geography (str) – Geographic level (‘state’, ‘county’, ‘tract’, ‘block_group’, ‘block’)

  • county (str | None) – Optional county FIPS code to filter to

  • tables (List[str] | None) – List of tables to include (e.g., [‘P1’, ‘P2’, ‘H1’]). If None, includes all tables.

Returns:

DataFrame with PL data at specified geography level

Return type:

pandas.DataFrame

clear_cache()[source]

Clear all cached PL files.

Return type:

int

siege_utilities.geo.census_files.pl_downloader.get_pl_data(state, year=2020, geography='tract', county=None, tables=None)[source]

Get PL 94-171 redistricting data.

Parameters:
  • state (str) – State name, abbreviation, or FIPS code

  • year (int) – Census year (2010 or 2020)

  • geography (str) – Geographic level

  • county (str | None) – Optional county FIPS filter

  • tables (List[str] | None) – Tables to include (default: all)

Returns:

DataFrame with PL data

Return type:

pandas.DataFrame

Example

# Get California tract data df = get_pl_data(state=’CA’, geography=’tract’)

# Get LA County block data df = get_pl_data(state=’CA’, geography=’block’, county=’037’)

siege_utilities.geo.census_files.pl_downloader.get_pl_blocks(state, county=None, year=2020, tables=None)[source]

Get block-level PL 94-171 data.

This is the primary function for redistricting analysis as it provides the finest geographic detail available.

Parameters:
  • state (str) – State identifier

  • county (str | None) – Optional county FIPS to filter

  • year (int) – Census year

  • tables (List[str] | None) – Tables to include

Returns:

DataFrame with block-level PL data

Return type:

pandas.DataFrame

siege_utilities.geo.census_files.pl_downloader.get_pl_tracts(state, county=None, year=2020, tables=None)[source]

Get tract-level PL 94-171 data.

Parameters:
  • state (str) – State identifier

  • county (str | None) – Optional county FIPS filter

  • year (int) – Census year

  • tables (List[str] | None) – Tables to include

Returns:

DataFrame with tract-level PL data

Return type:

pandas.DataFrame

siege_utilities.geo.census_files.pl_downloader.download_pl_file(state, year=2020, output_dir=None)[source]

Download raw PL 94-171 zip file.

Parameters:
  • state (str) – State identifier

  • year (int) – Census year

  • output_dir (str | Path | None) – Directory to save file

Returns:

Path to downloaded file

Return type:

Path

siege_utilities.geo.census_files.pl_downloader.list_available_pl_files(year=2020)[source]

List available PL 94-171 files.

Parameters:

year (int) – Census year

Returns:

List of available states with file info

Return type:

List[Dict]

Crosswalks and Interpolation

Census boundary crosswalk support for tracking geographic changes over time.

This module provides tools for working with Census boundary crosswalks, which describe how geographic units (tracts, block groups, etc.) changed between decennial census years (e.g., 2010 to 2020).

Key features: - Download and cache crosswalk files from Census Bureau - Apply crosswalks to transform data between boundary vintages - Identify which geographies were split, merged, or unchanged

Example usage:
from siege_utilities.geo.crosswalk import (

get_crosswalk, apply_crosswalk, identify_boundary_changes, WeightMethod

)

# Get the crosswalk for California tracts crosswalk = get_crosswalk(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

# Transform 2010 data to 2020 boundaries df_2020 = apply_crosswalk(

df=df_2010, source_year=2010, target_year=2020, geography_level=’tract’, weight_method=WeightMethod.AREA

)

# See which tracts changed changes = identify_boundary_changes(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

class siege_utilities.geo.crosswalk.RelationshipType[source]

Bases: Enum

Types of relationships between source and target geographies in crosswalks.

These describe how a geographic unit changed between Census years.

ONE_TO_ONE = 'one_to_one'

Geographic unit unchanged - maps directly to one target unit.

SPLIT = 'split'

One source unit split into multiple target units (one-to-many).

MERGED = 'merged'

Multiple source units merged into one target unit (many-to-one).

PARTIAL_OVERLAP = 'partial_overlap'

Complex relationship - parts of units overlap (many-to-many).

class siege_utilities.geo.crosswalk.WeightMethod[source]

Bases: Enum

Methods for weighting data when applying crosswalks.

Different methods are appropriate for different types of data: - AREA: Best for land-use data, environmental measures - POPULATION: Best for demographic data, social measures - HOUSING: Best for housing-related variables - EQUAL: Simple equal weighting (use when weights unavailable)

AREA = 'area'

Weight by land area proportion.

POPULATION = 'population'

Weight by population proportion (requires population data).

HOUSING = 'housing'

Weight by housing unit proportion.

EQUAL = 'equal'

Equal weighting across all relationships.

class siege_utilities.geo.crosswalk.CrosswalkRelationship[source]

Bases: object

Represents a single relationship between source and target geographies.

This is one row in a crosswalk file, describing how data should flow from a source geography (e.g., 2010 tract) to a target geography (e.g., 2020 tract).

source_geoid

GEOID of the source geography

Type:

str

target_geoid

GEOID of the target geography

Type:

str

relationship_type

Type of relationship (split, merge, etc.)

Type:

siege_utilities.geo.crosswalk.relationship_types.RelationshipType

area_weight

Proportion of source area in target (0.0 to 1.0)

Type:

float

population_weight

Proportion of source population in target

Type:

float | None

housing_weight

Proportion of source housing units in target

Type:

float | None

source_area

Land area of source geography (sq meters)

Type:

float | None

target_area

Land area of target geography (sq meters)

Type:

float | None

overlap_area

Area of intersection (sq meters)

Type:

float | None

source_geoid: str
target_geoid: str
relationship_type: RelationshipType = 'one_to_one'
area_weight: float = 1.0
population_weight: float | None = None
housing_weight: float | None = None
source_area: float | None = None
target_area: float | None = None
overlap_area: float | None = None
get_weight(method)[source]

Get the appropriate weight for the specified method.

Parameters:

method (WeightMethod) – Weighting method to use

Returns:

Weight value (0.0 to 1.0)

Raises:

ValueError – If requested weight type is not available

Return type:

float

__init__(source_geoid, target_geoid, relationship_type=RelationshipType.ONE_TO_ONE, area_weight=1.0, population_weight=None, housing_weight=None, source_area=None, target_area=None, overlap_area=None)
Parameters:
Return type:

None

class siege_utilities.geo.crosswalk.CrosswalkMetadata[source]

Bases: object

Metadata about a crosswalk dataset.

source_year

Year of source geographies (e.g., 2010)

Type:

int

target_year

Year of target geographies (e.g., 2020)

Type:

int

geography_level

Geographic level (e.g., ‘tract’, ‘block_group’)

Type:

str

state_fips

State FIPS code if state-specific, None for national

Type:

str | None

total_source_units

Number of unique source geographies

Type:

int

total_target_units

Number of unique target geographies

Type:

int

one_to_one_count

Number of unchanged geographies

Type:

int

split_count

Number of geographies that were split

Type:

int

merged_count

Number of geographies that were merged

Type:

int

source_url

URL where crosswalk was downloaded from

Type:

str | None

download_date

Date crosswalk was downloaded

Type:

str | None

source_year: int
target_year: int
geography_level: str
state_fips: str | None = None
total_source_units: int = 0
total_target_units: int = 0
one_to_one_count: int = 0
split_count: int = 0
merged_count: int = 0
source_url: str | None = None
download_date: str | None = None
property change_rate: float

Calculate the rate of geographic change.

Returns:

Proportion of geographies that changed (0.0 to 1.0)

summary()[source]

Get a summary dictionary of crosswalk statistics.

Returns:

Dictionary with crosswalk statistics

Return type:

Dict

__init__(source_year, target_year, geography_level, state_fips=None, total_source_units=0, total_target_units=0, one_to_one_count=0, split_count=0, merged_count=0, source_url=None, download_date=None)
Parameters:
  • source_year (int)

  • target_year (int)

  • geography_level (str)

  • state_fips (str | None)

  • total_source_units (int)

  • total_target_units (int)

  • one_to_one_count (int)

  • split_count (int)

  • merged_count (int)

  • source_url (str | None)

  • download_date (str | None)

Return type:

None

class siege_utilities.geo.crosswalk.GeographyChange[source]

Bases: object

Summary of changes for a single geography between Census years.

This provides a higher-level view than individual relationships, summarizing all the changes for a single source geography.

source_geoid

GEOID of the source geography

Type:

str

relationship_type

Overall relationship type

Type:

siege_utilities.geo.crosswalk.relationship_types.RelationshipType

target_geoids

List of target GEOIDs this geography maps to

Type:

List[str]

weights

Dictionary mapping target GEOIDs to their weights

Type:

Dict[str, float]

population_2010

Source geography population in 2010

Type:

int | None

population_2020

Total population of target geographies in 2020

Type:

int | None

source_geoid: str
relationship_type: RelationshipType
target_geoids: List[str]
weights: Dict[str, float]
population_2010: int | None = None
__init__(source_geoid, relationship_type, target_geoids=<factory>, weights=<factory>, population_2010=None, population_2020=None)
Parameters:
Return type:

None

population_2020: int | None = None
property is_unchanged: bool

Check if geography is unchanged.

property was_split: bool

Check if geography was split.

property was_merged: bool

Check if geography was merged.

property num_targets: int

Number of target geographies.

class siege_utilities.geo.crosswalk.CrosswalkClient[source]

Bases: object

Client for downloading and managing Census crosswalk files.

This client handles: - Downloading crosswalk files from Census Bureau - Caching files locally as parquet for fast access - Parsing pipe-delimited Census files - Filtering by state if needed

cache_dir

Directory for cached crosswalk files

timeout

Request timeout in seconds

__init__(cache_dir=None, timeout=120)[source]

Initialize the crosswalk client.

Parameters:
  • cache_dir (str | Path | None) – Directory for caching files (defaults to ~/.siege_utilities/cache/crosswalk)

  • timeout (int) – Request timeout in seconds

get_crosswalk(source_year=2010, target_year=2020, geography_level='tract', state_fips=None, force_refresh=False)[source]

Get a crosswalk DataFrame for the specified years and geography.

Parameters:
  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS code to filter results

  • force_refresh (bool) – If True, re-download even if cached

Returns:

  • source_geoid: GEOID in source year

  • target_geoid: GEOID in target year

  • area_weight: Proportion based on land area overlap

  • Additional metadata columns

Return type:

DataFrame with crosswalk relationships including

Raises:
  • ValueError – If year combination is not supported

  • requests.RequestException – If download fails

get_crosswalk_metadata(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Get metadata about a crosswalk.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

CrosswalkMetadata with statistics about the crosswalk

Return type:

CrosswalkMetadata

list_available_crosswalks()[source]

List all available crosswalk combinations.

Returns:

List of dictionaries describing available crosswalks

Return type:

List[Dict]

clear_cache(older_than_days=None)[source]

Clear cached crosswalk files.

Parameters:

older_than_days (int | None) – Only clear files older than this many days. If None, clear all files.

Returns:

Number of files deleted

Return type:

int

siege_utilities.geo.crosswalk.get_crosswalk(source_year=2010, target_year=2020, geography_level='tract', state_fips=None, force_refresh=False)[source]

Convenience function to get a crosswalk DataFrame.

Parameters:
  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS code to filter results

  • force_refresh (bool) – If True, re-download even if cached

Returns:

DataFrame with crosswalk relationships

Return type:

pandas.DataFrame

Example

# Get California tract crosswalk df = get_crosswalk(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

siege_utilities.geo.crosswalk.get_crosswalk_client()[source]

Get or create the default crosswalk client.

Return type:

CrosswalkClient

siege_utilities.geo.crosswalk.get_crosswalk_metadata(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Convenience function to get crosswalk metadata.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

CrosswalkMetadata with statistics

Return type:

CrosswalkMetadata

siege_utilities.geo.crosswalk.list_available_crosswalks()[source]

List all available crosswalk combinations.

Returns:

List of dictionaries describing available crosswalks

Return type:

List[Dict]

class siege_utilities.geo.crosswalk.CrosswalkProcessor[source]

Bases: object

Processor for applying crosswalks to transform data between Census years.

This class handles the logic of transforming data from one boundary vintage to another, accounting for splits, merges, and complex changes.

crosswalk_df

The crosswalk DataFrame with relationships

source_year

Source boundary year

target_year

Target boundary year

geography_level

Geographic level being processed

__init__(crosswalk_df, source_year, target_year, geography_level)[source]

Initialize the processor with a crosswalk.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame from get_crosswalk()

  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level (‘tract’, ‘county’, etc.)

get_relationship_type(source_geoid)[source]

Determine the relationship type for a source geography.

Parameters:

source_geoid (str) – GEOID in source year

Returns:

RelationshipType indicating how this geography changed

Return type:

RelationshipType

get_geography_change(source_geoid)[source]

Get detailed change information for a source geography.

Parameters:

source_geoid (str) – GEOID in source year

Returns:

GeographyChange object with relationship details

Return type:

GeographyChange

transform(df, geoid_column='GEOID', value_columns=None, weight_method=WeightMethod.AREA, aggregation_func='sum')[source]

Transform data from source boundaries to target boundaries.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data in source boundary vintage

  • geoid_column (str) – Name of the GEOID column

  • value_columns (List[str] | None) – List of columns to transform. If None, all numeric columns.

  • weight_method (WeightMethod) – How to weight data for splits/merges

  • aggregation_func (str | Callable) – How to aggregate merged data (‘sum’, ‘mean’, ‘weighted_mean’)

Returns:

DataFrame with data in target boundary vintage

Raises:

ValueError – If geoid_column not found in DataFrame

Return type:

pandas.DataFrame

siege_utilities.geo.crosswalk.apply_crosswalk(df, source_year=2010, target_year=2020, geography_level='tract', state_fips=None, geoid_column='GEOID', value_columns=None, weight_method=WeightMethod.AREA, aggregation_func='sum')[source]

Apply a crosswalk to transform data from one boundary vintage to another.

This is the main function for transforming data between Census years. It handles all relationship types (unchanged, splits, merges) automatically.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data in source boundary vintage

  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS to filter crosswalk

  • geoid_column (str) – Name of the GEOID column in df

  • value_columns (List[str] | None) – List of columns to transform. If None, all numeric columns.

  • weight_method (WeightMethod) – How to weight data for splits/merges

  • aggregation_func (str | Callable) – How to aggregate data (‘sum’, ‘mean’, ‘weighted_mean’)

Returns:

DataFrame with data transformed to target boundary vintage

Return type:

pandas.DataFrame

Example

# Transform 2010 income data to 2020 tracts df_2020 = apply_crosswalk(

df=income_2010, source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’, # California value_columns=[‘median_income’, ‘total_population’]

)

siege_utilities.geo.crosswalk.normalize_to_year(df, data_year, target_year=2020, geography_level='tract', state_fips=None, geoid_column='GEOID', value_columns=None)[source]

Normalize data to a specific boundary year.

Convenience function that determines the correct crosswalk direction and applies it to transform data to the target year’s boundaries.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data

  • data_year (int) – Year of the data’s boundaries

  • target_year (int) – Year to normalize to (default: 2020)

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

  • geoid_column (str) – Name of GEOID column

  • value_columns (List[str] | None) – Columns to transform

Returns:

DataFrame normalized to target year boundaries

Return type:

pandas.DataFrame

Example

# Normalize mixed-year data to 2020 boundaries df_normalized = normalize_to_year(

df=old_data, data_year=2010, target_year=2020, geography_level=’tract’

)

siege_utilities.geo.crosswalk.identify_boundary_changes(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Identify and summarize boundary changes between Census years.

Returns a DataFrame showing how each source geography changed, useful for understanding the extent of boundary changes in an area.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

  • source_geoid: GEOID in source year

  • relationship_type: Type of change (unchanged, split, merged)

  • num_targets: Number of target GEOIDs this maps to

  • target_geoids: List of target GEOIDs (as string)

Return type:

DataFrame with columns

Example

# See which California tracts changed changes = identify_boundary_changes(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

# Filter to just splits splits = changes[changes[‘relationship_type’] == ‘split’]

siege_utilities.geo.crosswalk.get_split_tracts(source_year=2010, target_year=2020, state_fips=None)[source]

Get a list of tracts that were split between Census years.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • state_fips (str | None) – Optional state FIPS filter

Returns:

DataFrame with split tracts and their target tracts

Return type:

pandas.DataFrame

siege_utilities.geo.crosswalk.get_merged_tracts(source_year=2010, target_year=2020, state_fips=None)[source]

Get a list of tracts that were merged between Census years.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • state_fips (str | None) – Optional state FIPS filter

Returns:

DataFrame with merged tracts and their resulting tract

Return type:

pandas.DataFrame

Client for downloading and caching Census boundary crosswalk files.

This module provides functions to download crosswalk files from the Census Bureau and cache them locally for efficient repeated access.

Census crosswalk files describe how geographic boundaries changed between decennial census years. They are essential for: - Comparing data across different Census years - Tracking demographic changes in consistent geographies - Normalizing historical data to current boundaries

Example usage:

from siege_utilities.geo.crosswalk import get_crosswalk

# Get 2010-2020 tract crosswalk for California crosswalk_df = get_crosswalk(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

# View relationships print(crosswalk_df.head())

class siege_utilities.geo.crosswalk.crosswalk_client.CrosswalkClient[source]

Bases: object

Client for downloading and managing Census crosswalk files.

This client handles: - Downloading crosswalk files from Census Bureau - Caching files locally as parquet for fast access - Parsing pipe-delimited Census files - Filtering by state if needed

cache_dir

Directory for cached crosswalk files

timeout

Request timeout in seconds

__init__(cache_dir=None, timeout=120)[source]

Initialize the crosswalk client.

Parameters:
  • cache_dir (str | Path | None) – Directory for caching files (defaults to ~/.siege_utilities/cache/crosswalk)

  • timeout (int) – Request timeout in seconds

get_crosswalk(source_year=2010, target_year=2020, geography_level='tract', state_fips=None, force_refresh=False)[source]

Get a crosswalk DataFrame for the specified years and geography.

Parameters:
  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS code to filter results

  • force_refresh (bool) – If True, re-download even if cached

Returns:

  • source_geoid: GEOID in source year

  • target_geoid: GEOID in target year

  • area_weight: Proportion based on land area overlap

  • Additional metadata columns

Return type:

DataFrame with crosswalk relationships including

Raises:
  • ValueError – If year combination is not supported

  • requests.RequestException – If download fails

get_crosswalk_metadata(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Get metadata about a crosswalk.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

CrosswalkMetadata with statistics about the crosswalk

Return type:

CrosswalkMetadata

list_available_crosswalks()[source]

List all available crosswalk combinations.

Returns:

List of dictionaries describing available crosswalks

Return type:

List[Dict]

clear_cache(older_than_days=None)[source]

Clear cached crosswalk files.

Parameters:

older_than_days (int | None) – Only clear files older than this many days. If None, clear all files.

Returns:

Number of files deleted

Return type:

int

siege_utilities.geo.crosswalk.crosswalk_client.get_crosswalk_client()[source]

Get or create the default crosswalk client.

Return type:

CrosswalkClient

siege_utilities.geo.crosswalk.crosswalk_client.get_crosswalk(source_year=2010, target_year=2020, geography_level='tract', state_fips=None, force_refresh=False)[source]

Convenience function to get a crosswalk DataFrame.

Parameters:
  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS code to filter results

  • force_refresh (bool) – If True, re-download even if cached

Returns:

DataFrame with crosswalk relationships

Return type:

pandas.DataFrame

Example

# Get California tract crosswalk df = get_crosswalk(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

siege_utilities.geo.crosswalk.crosswalk_client.get_crosswalk_metadata(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Convenience function to get crosswalk metadata.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

CrosswalkMetadata with statistics

Return type:

CrosswalkMetadata

siege_utilities.geo.crosswalk.crosswalk_client.list_available_crosswalks()[source]

List all available crosswalk combinations.

Returns:

List of dictionaries describing available crosswalks

Return type:

List[Dict]

Crosswalk processor for transforming data between Census boundary years.

This module provides functions to apply crosswalk relationships to transform data from one Census boundary vintage to another. It handles:

  • One-to-one mappings: Simple GEOID rename

  • One-to-many (splits): Disaggregate data using weights

  • Many-to-one (merges): Aggregate data using weights

Example usage:

from siege_utilities.geo.crosswalk import apply_crosswalk, WeightMethod

# Transform 2010 data to 2020 boundaries df_2020 = apply_crosswalk(

df=df_2010, source_year=2010, target_year=2020, weight_method=WeightMethod.AREA, aggregation_func=’sum’

)

class siege_utilities.geo.crosswalk.crosswalk_processor.CrosswalkProcessor[source]

Bases: object

Processor for applying crosswalks to transform data between Census years.

This class handles the logic of transforming data from one boundary vintage to another, accounting for splits, merges, and complex changes.

crosswalk_df

The crosswalk DataFrame with relationships

source_year

Source boundary year

target_year

Target boundary year

geography_level

Geographic level being processed

__init__(crosswalk_df, source_year, target_year, geography_level)[source]

Initialize the processor with a crosswalk.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame from get_crosswalk()

  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level (‘tract’, ‘county’, etc.)

get_relationship_type(source_geoid)[source]

Determine the relationship type for a source geography.

Parameters:

source_geoid (str) – GEOID in source year

Returns:

RelationshipType indicating how this geography changed

Return type:

RelationshipType

get_geography_change(source_geoid)[source]

Get detailed change information for a source geography.

Parameters:

source_geoid (str) – GEOID in source year

Returns:

GeographyChange object with relationship details

Return type:

GeographyChange

transform(df, geoid_column='GEOID', value_columns=None, weight_method=WeightMethod.AREA, aggregation_func='sum')[source]

Transform data from source boundaries to target boundaries.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data in source boundary vintage

  • geoid_column (str) – Name of the GEOID column

  • value_columns (List[str] | None) – List of columns to transform. If None, all numeric columns.

  • weight_method (WeightMethod) – How to weight data for splits/merges

  • aggregation_func (str | Callable) – How to aggregate merged data (‘sum’, ‘mean’, ‘weighted_mean’)

Returns:

DataFrame with data in target boundary vintage

Raises:

ValueError – If geoid_column not found in DataFrame

Return type:

pandas.DataFrame

siege_utilities.geo.crosswalk.crosswalk_processor.apply_crosswalk(df, source_year=2010, target_year=2020, geography_level='tract', state_fips=None, geoid_column='GEOID', value_columns=None, weight_method=WeightMethod.AREA, aggregation_func='sum')[source]

Apply a crosswalk to transform data from one boundary vintage to another.

This is the main function for transforming data between Census years. It handles all relationship types (unchanged, splits, merges) automatically.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data in source boundary vintage

  • source_year (int) – Source Census year (e.g., 2010)

  • target_year (int) – Target Census year (e.g., 2020)

  • geography_level (str) – Geographic level (‘tract’, ‘block_group’, ‘county’)

  • state_fips (str | None) – Optional state FIPS to filter crosswalk

  • geoid_column (str) – Name of the GEOID column in df

  • value_columns (List[str] | None) – List of columns to transform. If None, all numeric columns.

  • weight_method (WeightMethod) – How to weight data for splits/merges

  • aggregation_func (str | Callable) – How to aggregate data (‘sum’, ‘mean’, ‘weighted_mean’)

Returns:

DataFrame with data transformed to target boundary vintage

Return type:

pandas.DataFrame

Example

# Transform 2010 income data to 2020 tracts df_2020 = apply_crosswalk(

df=income_2010, source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’, # California value_columns=[‘median_income’, ‘total_population’]

)

siege_utilities.geo.crosswalk.crosswalk_processor.normalize_to_year(df, data_year, target_year=2020, geography_level='tract', state_fips=None, geoid_column='GEOID', value_columns=None)[source]

Normalize data to a specific boundary year.

Convenience function that determines the correct crosswalk direction and applies it to transform data to the target year’s boundaries.

Parameters:
  • df (pandas.DataFrame) – DataFrame with data

  • data_year (int) – Year of the data’s boundaries

  • target_year (int) – Year to normalize to (default: 2020)

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

  • geoid_column (str) – Name of GEOID column

  • value_columns (List[str] | None) – Columns to transform

Returns:

DataFrame normalized to target year boundaries

Return type:

pandas.DataFrame

Example

# Normalize mixed-year data to 2020 boundaries df_normalized = normalize_to_year(

df=old_data, data_year=2010, target_year=2020, geography_level=’tract’

)

siege_utilities.geo.crosswalk.crosswalk_processor.identify_boundary_changes(source_year=2010, target_year=2020, geography_level='tract', state_fips=None)[source]

Identify and summarize boundary changes between Census years.

Returns a DataFrame showing how each source geography changed, useful for understanding the extent of boundary changes in an area.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • geography_level (str) – Geographic level

  • state_fips (str | None) – Optional state FIPS filter

Returns:

  • source_geoid: GEOID in source year

  • relationship_type: Type of change (unchanged, split, merged)

  • num_targets: Number of target GEOIDs this maps to

  • target_geoids: List of target GEOIDs (as string)

Return type:

DataFrame with columns

Example

# See which California tracts changed changes = identify_boundary_changes(

source_year=2010, target_year=2020, geography_level=’tract’, state_fips=’06’

)

# Filter to just splits splits = changes[changes[‘relationship_type’] == ‘split’]

siege_utilities.geo.crosswalk.crosswalk_processor.get_split_tracts(source_year=2010, target_year=2020, state_fips=None)[source]

Get a list of tracts that were split between Census years.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • state_fips (str | None) – Optional state FIPS filter

Returns:

DataFrame with split tracts and their target tracts

Return type:

pandas.DataFrame

siege_utilities.geo.crosswalk.crosswalk_processor.get_merged_tracts(source_year=2010, target_year=2020, state_fips=None)[source]

Get a list of tracts that were merged between Census years.

Parameters:
  • source_year (int) – Source Census year

  • target_year (int) – Target Census year

  • state_fips (str | None) – Optional state FIPS filter

Returns:

DataFrame with merged tracts and their resulting tract

Return type:

pandas.DataFrame

Relationship types and data classes for Census boundary crosswalks.

This module defines enumerations and data classes that describe how Census geographic boundaries change between decennial census years (e.g., 2010 to 2020).

Census tracts can: - Remain unchanged (ONE_TO_ONE) - Be split into multiple tracts (ONE_TO_MANY) - Be merged with other tracts (MANY_TO_ONE)

The crosswalk files from Census Bureau provide relationship weights based on: - Land area proportions - Population distribution

Example usage:

from siege_utilities.geo.crosswalk import RelationshipType, WeightMethod

# Check relationship type if relationship == RelationshipType.SPLIT:

# Handle tract that was split …

# Specify weighting method method = WeightMethod.POPULATION

class siege_utilities.geo.crosswalk.relationship_types.RelationshipType[source]

Bases: Enum

Types of relationships between source and target geographies in crosswalks.

These describe how a geographic unit changed between Census years.

ONE_TO_ONE = 'one_to_one'

Geographic unit unchanged - maps directly to one target unit.

SPLIT = 'split'

One source unit split into multiple target units (one-to-many).

MERGED = 'merged'

Multiple source units merged into one target unit (many-to-one).

PARTIAL_OVERLAP = 'partial_overlap'

Complex relationship - parts of units overlap (many-to-many).

class siege_utilities.geo.crosswalk.relationship_types.WeightMethod[source]

Bases: Enum

Methods for weighting data when applying crosswalks.

Different methods are appropriate for different types of data: - AREA: Best for land-use data, environmental measures - POPULATION: Best for demographic data, social measures - HOUSING: Best for housing-related variables - EQUAL: Simple equal weighting (use when weights unavailable)

AREA = 'area'

Weight by land area proportion.

POPULATION = 'population'

Weight by population proportion (requires population data).

HOUSING = 'housing'

Weight by housing unit proportion.

EQUAL = 'equal'

Equal weighting across all relationships.

class siege_utilities.geo.crosswalk.relationship_types.CrosswalkRelationship[source]

Bases: object

Represents a single relationship between source and target geographies.

This is one row in a crosswalk file, describing how data should flow from a source geography (e.g., 2010 tract) to a target geography (e.g., 2020 tract).

source_geoid

GEOID of the source geography

Type:

str

target_geoid

GEOID of the target geography

Type:

str

relationship_type

Type of relationship (split, merge, etc.)

Type:

siege_utilities.geo.crosswalk.relationship_types.RelationshipType

area_weight

Proportion of source area in target (0.0 to 1.0)

Type:

float

population_weight

Proportion of source population in target

Type:

float | None

housing_weight

Proportion of source housing units in target

Type:

float | None

source_area

Land area of source geography (sq meters)

Type:

float | None

target_area

Land area of target geography (sq meters)

Type:

float | None

overlap_area

Area of intersection (sq meters)

Type:

float | None

source_geoid: str
target_geoid: str
relationship_type: RelationshipType = 'one_to_one'
area_weight: float = 1.0
population_weight: float | None = None
housing_weight: float | None = None
source_area: float | None = None
target_area: float | None = None
overlap_area: float | None = None
get_weight(method)[source]

Get the appropriate weight for the specified method.

Parameters:

method (WeightMethod) – Weighting method to use

Returns:

Weight value (0.0 to 1.0)

Raises:

ValueError – If requested weight type is not available

Return type:

float

__init__(source_geoid, target_geoid, relationship_type=RelationshipType.ONE_TO_ONE, area_weight=1.0, population_weight=None, housing_weight=None, source_area=None, target_area=None, overlap_area=None)
Parameters:
Return type:

None

class siege_utilities.geo.crosswalk.relationship_types.CrosswalkMetadata[source]

Bases: object

Metadata about a crosswalk dataset.

source_year

Year of source geographies (e.g., 2010)

Type:

int

target_year

Year of target geographies (e.g., 2020)

Type:

int

geography_level

Geographic level (e.g., ‘tract’, ‘block_group’)

Type:

str

state_fips

State FIPS code if state-specific, None for national

Type:

str | None

total_source_units

Number of unique source geographies

Type:

int

total_target_units

Number of unique target geographies

Type:

int

one_to_one_count

Number of unchanged geographies

Type:

int

split_count

Number of geographies that were split

Type:

int

merged_count

Number of geographies that were merged

Type:

int

source_url

URL where crosswalk was downloaded from

Type:

str | None

download_date

Date crosswalk was downloaded

Type:

str | None

source_year: int
target_year: int
geography_level: str
state_fips: str | None = None
total_source_units: int = 0
total_target_units: int = 0
one_to_one_count: int = 0
split_count: int = 0
merged_count: int = 0
source_url: str | None = None
download_date: str | None = None
property change_rate: float

Calculate the rate of geographic change.

Returns:

Proportion of geographies that changed (0.0 to 1.0)

summary()[source]

Get a summary dictionary of crosswalk statistics.

Returns:

Dictionary with crosswalk statistics

Return type:

Dict

__init__(source_year, target_year, geography_level, state_fips=None, total_source_units=0, total_target_units=0, one_to_one_count=0, split_count=0, merged_count=0, source_url=None, download_date=None)
Parameters:
  • source_year (int)

  • target_year (int)

  • geography_level (str)

  • state_fips (str | None)

  • total_source_units (int)

  • total_target_units (int)

  • one_to_one_count (int)

  • split_count (int)

  • merged_count (int)

  • source_url (str | None)

  • download_date (str | None)

Return type:

None

class siege_utilities.geo.crosswalk.relationship_types.GeographyChange[source]

Bases: object

Summary of changes for a single geography between Census years.

This provides a higher-level view than individual relationships, summarizing all the changes for a single source geography.

source_geoid

GEOID of the source geography

Type:

str

relationship_type

Overall relationship type

Type:

siege_utilities.geo.crosswalk.relationship_types.RelationshipType

target_geoids

List of target GEOIDs this geography maps to

Type:

List[str]

weights

Dictionary mapping target GEOIDs to their weights

Type:

Dict[str, float]

population_2010

Source geography population in 2010

Type:

int | None

population_2020

Total population of target geographies in 2020

Type:

int | None

source_geoid: str
relationship_type: RelationshipType
target_geoids: List[str]
weights: Dict[str, float]
population_2010: int | None = None
__init__(source_geoid, relationship_type, target_geoids=<factory>, weights=<factory>, population_2010=None, population_2020=None)
Parameters:
Return type:

None

population_2020: int | None = None
property is_unchanged: bool

Check if geography is unchanged.

property was_split: bool

Check if geography was split.

property was_merged: bool

Check if geography was merged.

property num_targets: int

Number of target geographies.

Areal interpolation for transferring data between geographic boundaries.

Wraps PySAL’s Tobler library to provide a consistent interface for extensive (population) and intensive (median income) variable interpolation.

Example:

from siege_utilities.geo.interpolation import interpolate_extensive, interpolate_intensive

# Transfer population counts from 2010 tracts to 2020 tracts
result = interpolate_extensive(
    source_gdf=tracts_2010,
    target_gdf=tracts_2020,
    variables=["total_pop", "housing_units"],
)

# Transfer median income (rate) from 2010 to 2020 tracts
result = interpolate_intensive(
    source_gdf=tracts_2010,
    target_gdf=tracts_2020,
    variables=["median_income", "poverty_rate"],
)
class siege_utilities.geo.interpolation.ArealInterpolationResult[source]

Bases: object

Result of an areal interpolation operation.

data

GeoDataFrame with interpolated values in target geometries.

Type:

geopandas.GeoDataFrame

extensive_variables

List of extensive variables interpolated.

Type:

list[str]

intensive_variables

List of intensive variables interpolated.

Type:

list[str]

source_crs

CRS of the source data.

Type:

str | None

target_crs

CRS of the target data.

Type:

str | None

n_source

Number of source polygons.

Type:

int

n_target

Number of target polygons.

Type:

int

warnings

Any warnings generated during interpolation.

Type:

list[str]

backend

Which backend performed the interpolation.

Type:

str

data: geopandas.GeoDataFrame
extensive_variables: list[str]
intensive_variables: list[str]
source_crs: str | None = None
target_crs: str | None = None
n_source: int = 0
n_target: int = 0
warnings: list[str]
backend: str = ''
__init__(data, extensive_variables=<factory>, intensive_variables=<factory>, source_crs=None, target_crs=None, n_source=0, n_target=0, warnings=<factory>, backend='')
Parameters:
Return type:

None

siege_utilities.geo.interpolation.interpolate_areal(source_gdf, target_gdf, extensive_variables=None, intensive_variables=None, allocate_total=True, n_jobs=1, *, crs=None)[source]

Transfer attribute data between non-coincident geographies.

Uses area-weighted interpolation to redistribute values from source polygons to target polygons based on the area of intersection.

Backend dispatch (in priority order): 1. tobler (PySAL) — when tobler + geopandas are installed 2. DuckDB spatial — when duckdb is installed (no GDAL needed) 3. pure Shapely — STRtree + intersection (always available with geopandas)

Parameters:
  • source_gdf (geopandas.GeoDataFrame) – Source GeoDataFrame with attribute columns.

  • target_gdf (geopandas.GeoDataFrame) – Target GeoDataFrame defining output geometries.

  • extensive_variables (list[str] | None) – Columns containing totals (population, etc.). These are split proportionally by area overlap.

  • intensive_variables (list[str] | None) – Columns containing rates/densities. These are area-weighted averaged.

  • allocate_total (bool) – If True, ensure 100% of source area is allocated (only applies to tobler backend).

  • n_jobs (int) – Number of parallel jobs (only applies to tobler backend).

  • crs (str | None) – Output CRS for the result GeoDataFrame (default: source CRS).

Returns:

ArealInterpolationResult with interpolated GeoDataFrame in crs.

Raises:
  • ImportError – If no suitable backend is available.

  • ValueError – If no variables are specified or columns missing.

Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.interpolate_extensive(source_gdf, target_gdf, variables, allocate_total=True, n_jobs=1, *, crs=None)[source]

Interpolate extensive (total/count) variables between geographies.

Parameters:
Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.interpolate_intensive(source_gdf, target_gdf, variables, allocate_total=True, n_jobs=1, *, crs=None)[source]

Interpolate intensive (rate/density) variables between geographies.

Parameters:
Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.compute_area_weights(source_gdf, target_gdf, *, crs=None)[source]

Compute the area overlap matrix between source and target polygons.

Returns a GeoDataFrame with columns: - source_idx: Index of the source polygon. - target_idx: Index of the target polygon. - overlap_area: Area of intersection. - source_fraction: Fraction of source polygon covered. - target_fraction: Fraction of target polygon covered.

Parameters:
Returns:

GeoDataFrame with overlap weights in crs.

Return type:

geopandas.GeoDataFrame

Areal interpolation using PySAL’s Tobler library.

Provides functions to transfer attribute data between non-coincident geographic boundaries using area-weighted methods.

Two types of variables are handled: - Extensive (e.g., population, housing units): totals that should be

split proportionally when a source polygon overlaps multiple targets.

  • Intensive (e.g., median income, poverty rate): rates/densities that should be area-weighted averaged across overlapping sources.

Backend dispatch (in priority order): 1. tobler — PySAL’s area_interpolate (requires geopandas + tobler) 2. duckdb — DuckDB spatial extension (ST_Intersection / ST_Area) 3. shapely — Pure Shapely STRtree + intersection (always available)

class siege_utilities.geo.interpolation.areal.ArealInterpolationResult[source]

Bases: object

Result of an areal interpolation operation.

data

GeoDataFrame with interpolated values in target geometries.

Type:

geopandas.GeoDataFrame

extensive_variables

List of extensive variables interpolated.

Type:

list[str]

intensive_variables

List of intensive variables interpolated.

Type:

list[str]

source_crs

CRS of the source data.

Type:

str | None

target_crs

CRS of the target data.

Type:

str | None

n_source

Number of source polygons.

Type:

int

n_target

Number of target polygons.

Type:

int

warnings

Any warnings generated during interpolation.

Type:

list[str]

backend

Which backend performed the interpolation.

Type:

str

data: geopandas.GeoDataFrame
extensive_variables: list[str]
intensive_variables: list[str]
source_crs: str | None = None
target_crs: str | None = None
n_source: int = 0
n_target: int = 0
warnings: list[str]
backend: str = ''
__init__(data, extensive_variables=<factory>, intensive_variables=<factory>, source_crs=None, target_crs=None, n_source=0, n_target=0, warnings=<factory>, backend='')
Parameters:
Return type:

None

siege_utilities.geo.interpolation.areal.interpolate_areal(source_gdf, target_gdf, extensive_variables=None, intensive_variables=None, allocate_total=True, n_jobs=1, *, crs=None)[source]

Transfer attribute data between non-coincident geographies.

Uses area-weighted interpolation to redistribute values from source polygons to target polygons based on the area of intersection.

Backend dispatch (in priority order): 1. tobler (PySAL) — when tobler + geopandas are installed 2. DuckDB spatial — when duckdb is installed (no GDAL needed) 3. pure Shapely — STRtree + intersection (always available with geopandas)

Parameters:
  • source_gdf (geopandas.GeoDataFrame) – Source GeoDataFrame with attribute columns.

  • target_gdf (geopandas.GeoDataFrame) – Target GeoDataFrame defining output geometries.

  • extensive_variables (list[str] | None) – Columns containing totals (population, etc.). These are split proportionally by area overlap.

  • intensive_variables (list[str] | None) – Columns containing rates/densities. These are area-weighted averaged.

  • allocate_total (bool) – If True, ensure 100% of source area is allocated (only applies to tobler backend).

  • n_jobs (int) – Number of parallel jobs (only applies to tobler backend).

  • crs (str | None) – Output CRS for the result GeoDataFrame (default: source CRS).

Returns:

ArealInterpolationResult with interpolated GeoDataFrame in crs.

Raises:
  • ImportError – If no suitable backend is available.

  • ValueError – If no variables are specified or columns missing.

Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.areal.interpolate_extensive(source_gdf, target_gdf, variables, allocate_total=True, n_jobs=1, *, crs=None)[source]

Interpolate extensive (total/count) variables between geographies.

Parameters:
Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.areal.interpolate_intensive(source_gdf, target_gdf, variables, allocate_total=True, n_jobs=1, *, crs=None)[source]

Interpolate intensive (rate/density) variables between geographies.

Parameters:
Return type:

ArealInterpolationResult

siege_utilities.geo.interpolation.areal.compute_area_weights(source_gdf, target_gdf, *, crs=None)[source]

Compute the area overlap matrix between source and target polygons.

Returns a GeoDataFrame with columns: - source_idx: Index of the source polygon. - target_idx: Index of the target polygon. - overlap_area: Area of intersection. - source_fraction: Fraction of source polygon covered. - target_fraction: Fraction of target polygon covered.

Parameters:
Returns:

GeoDataFrame with overlap weights in crs.

Return type:

geopandas.GeoDataFrame

Gazetteers and Place History

Gazetteers — name → geometry resolution (ELE-2483).

Public API:

The Etter integration (etter_to_geometry()) consumes this interface — see siege_utilities.geo.providers.etter_filter.

class siege_utilities.geo.gazetteers.Gazetteer[source]

Bases: Protocol

Name → geometry resolver.

The protocol is intentionally narrow — four methods. Backends layer LRU caches, ISO-code translation, and other plumbing internally; callers just see this surface.

property provider_name: str

Human-readable backend identifier ("wkls", etc.).

lookup(name, *, country_hint=None, admin_hint=None)[source]

Best match for name.

Parameters:
  • name (str) – Place name in human form ("Birmingham, AL").

  • country_hint (str | None) – Optional ISO-3166-1 alpha-2 ("US") or alpha-3 ("USA") hint to disambiguate places that exist in multiple countries.

  • admin_hint (str | None) – Optional region/state hint ("Alabama", "AL", "01").

Raises:
Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]

Top-N candidate matches.

Used to surface ambiguity to a caller (interactive UI, or programmatic disambiguation when the caller has external knowledge to pick a candidate).

Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

is_available()[source]

Return True if the backend’s dependencies are installed and the backend can answer queries.

Should NOT make a network round-trip — this is a fast probe used by the factory to pick a default backend.

Return type:

bool

__init__(*args, **kwargs)
class siege_utilities.geo.gazetteers.GazetteerCandidate[source]

Bases: object

A search hit — a place that might be the answer.

Search returns multiple of these; lookup picks one. Keep the structure small and JSON-safe so it round-trips through APIs and logs cleanly.

name: str
canonical_path: tuple[str, ...]
country: str | None = None
admin_levels: Mapping[str, str]
score: float | None = None
source: str = ''
__init__(name, canonical_path, country=None, admin_levels=<factory>, score=None, source='')
Parameters:
Return type:

None

class siege_utilities.geo.gazetteers.GazetteerResult[source]

Bases: object

A resolved place: name, hierarchy, geometry, centroid.

name

Display name of the matched place.

Type:

str

canonical_path

Hierarchical breadcrumb the source resolves the place through, e.g. ("US", "AL", "Birmingham"). Useful for downstream filtering and for re-querying the same backend without going through fuzzy search again.

Type:

tuple[str, …]

geometry

shapely shape (Polygon / MultiPolygon for admin areas; may be a Point for cities-as-points).

Type:

Any

centroid

shapely Point. Pre-computed because half the consumer code needs it and recomputing on every call is wasteful.

Type:

Any

country

Optional ISO-3166-1 alpha-2 country code.

Type:

str | None

admin_levels

Optional mapping of admin level name → value (e.g. {"region": "Alabama", "county": "Jefferson"}). Backend-specific; not assumed by the resolver layer.

Type:

Mapping[str, str]

source

Backend name ("wkls", "nominatim", etc.) — for audit / log lineage.

Type:

str

raw

Backend-native object for callers that need full fidelity. Excluded from repr and hash so the dataclass stays sane to print and stable as a cache key.

Type:

Any

name: str
canonical_path: tuple[str, ...]
geometry: Any
centroid: Any
country: str | None = None
admin_levels: Mapping[str, str]
source: str = ''
raw: Any = None
__init__(name, canonical_path, geometry, centroid, country=None, admin_levels=<factory>, source='', raw=None)
Parameters:
Return type:

None

exception siege_utilities.geo.gazetteers.GazetteerError[source]

Bases: RuntimeError

Base class for all gazetteer failures.

exception siege_utilities.geo.gazetteers.GazetteerNotFoundError[source]

Bases: GazetteerError

No place matched the lookup criteria.

exception siege_utilities.geo.gazetteers.GazetteerAmbiguousError[source]

Bases: GazetteerError

The query is ambiguous and the caller didn’t pass enough hints.

Carries the candidates so the caller can surface them to a user or pick programmatically.

__init__(message, candidates)[source]
Parameters:
exception siege_utilities.geo.gazetteers.GazetteerBackendError[source]

Bases: GazetteerError

The backend raised an error we couldn’t translate.

Network failure, malformed upstream response, missing optional dependency at lookup time. Always chains the original via __cause__.

siege_utilities.geo.gazetteers.resolve_gazetteer(*, prefer=None, cache_size=1024)[source]

Return a configured Gazetteer backend.

Selection order:

  1. If prefer is given, use that backend (raises if unavailable).

  2. Otherwise, try WKLS (global coverage, no API key).

  3. Then Nominatim (OSM-backed, public service).

  4. Raise if nothing works.

Parameters:
  • prefer (str | None) – "wkls" / "nominatim" / "census" / "wikidata" to force a specific backend.

  • cache_size (int) – LRU cache size for backends that support it.

Raises:
Return type:

Gazetteer

Gazetteer protocol — name → geometry resolution.

A gazetteer takes a place name ("Birmingham, AL", "Lausanne", "Texas") and returns a structured GazetteerResult carrying the canonical hierarchy path, the geometry, and a centroid. Backends live in sibling modules (wkls_gazetteer, nominatim_gazetteer, etc.) and are selected via the factory in siege_utilities.geo.gazetteers.

The protocol is typed-Protocol rather than an ABC so backends can be plain classes without inheritance ceremony — useful for adapters wrapping foreign libraries.

Failure-mode discipline (matches the rest of siege_utilities, see docs/FAILURE_MODES.md): every method raises a typed exception on failure rather than returning None or an empty container that silently looks like a successful empty result.

class siege_utilities.geo.gazetteers.base.Gazetteer[source]

Bases: Protocol

Name → geometry resolver.

The protocol is intentionally narrow — four methods. Backends layer LRU caches, ISO-code translation, and other plumbing internally; callers just see this surface.

property provider_name: str

Human-readable backend identifier ("wkls", etc.).

lookup(name, *, country_hint=None, admin_hint=None)[source]

Best match for name.

Parameters:
  • name (str) – Place name in human form ("Birmingham, AL").

  • country_hint (str | None) – Optional ISO-3166-1 alpha-2 ("US") or alpha-3 ("USA") hint to disambiguate places that exist in multiple countries.

  • admin_hint (str | None) – Optional region/state hint ("Alabama", "AL", "01").

Raises:
Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]

Top-N candidate matches.

Used to surface ambiguity to a caller (interactive UI, or programmatic disambiguation when the caller has external knowledge to pick a candidate).

Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

is_available()[source]

Return True if the backend’s dependencies are installed and the backend can answer queries.

Should NOT make a network round-trip — this is a fast probe used by the factory to pick a default backend.

Return type:

bool

__init__(*args, **kwargs)
class siege_utilities.geo.gazetteers.base.GazetteerResult[source]

Bases: object

A resolved place: name, hierarchy, geometry, centroid.

name

Display name of the matched place.

Type:

str

canonical_path

Hierarchical breadcrumb the source resolves the place through, e.g. ("US", "AL", "Birmingham"). Useful for downstream filtering and for re-querying the same backend without going through fuzzy search again.

Type:

tuple[str, …]

geometry

shapely shape (Polygon / MultiPolygon for admin areas; may be a Point for cities-as-points).

Type:

Any

centroid

shapely Point. Pre-computed because half the consumer code needs it and recomputing on every call is wasteful.

Type:

Any

country

Optional ISO-3166-1 alpha-2 country code.

Type:

str | None

admin_levels

Optional mapping of admin level name → value (e.g. {"region": "Alabama", "county": "Jefferson"}). Backend-specific; not assumed by the resolver layer.

Type:

Mapping[str, str]

source

Backend name ("wkls", "nominatim", etc.) — for audit / log lineage.

Type:

str

raw

Backend-native object for callers that need full fidelity. Excluded from repr and hash so the dataclass stays sane to print and stable as a cache key.

Type:

Any

name: str
canonical_path: tuple[str, ...]
geometry: Any
centroid: Any
country: str | None = None
admin_levels: Mapping[str, str]
source: str = ''
raw: Any = None
__init__(name, canonical_path, geometry, centroid, country=None, admin_levels=<factory>, source='', raw=None)
Parameters:
Return type:

None

class siege_utilities.geo.gazetteers.base.GazetteerCandidate[source]

Bases: object

A search hit — a place that might be the answer.

Search returns multiple of these; lookup picks one. Keep the structure small and JSON-safe so it round-trips through APIs and logs cleanly.

name: str
canonical_path: tuple[str, ...]
country: str | None = None
admin_levels: Mapping[str, str]
score: float | None = None
source: str = ''
__init__(name, canonical_path, country=None, admin_levels=<factory>, score=None, source='')
Parameters:
Return type:

None

exception siege_utilities.geo.gazetteers.base.GazetteerError[source]

Bases: RuntimeError

Base class for all gazetteer failures.

exception siege_utilities.geo.gazetteers.base.GazetteerNotFoundError[source]

Bases: GazetteerError

No place matched the lookup criteria.

exception siege_utilities.geo.gazetteers.base.GazetteerAmbiguousError[source]

Bases: GazetteerError

The query is ambiguous and the caller didn’t pass enough hints.

Carries the candidates so the caller can surface them to a user or pick programmatically.

__init__(message, candidates)[source]
Parameters:
exception siege_utilities.geo.gazetteers.base.GazetteerBackendError[source]

Bases: GazetteerError

The backend raised an error we couldn’t translate.

Network failure, malformed upstream response, missing optional dependency at lookup time. Always chains the original via __cause__.

Census Geocoder + TIGERWeb gazetteer.

US-only. Two upstreams chained:

  1. Census Geocoder’s onelineaddress-style endpoint accepts a place name and returns matching geographies with FIPS codes (state + county + place + tract).

  2. TIGERWeb ArcGIS REST returns the polygon for a given (layer, FIPS) pair.

The split exists because Census Geocoder is address-centric and only returns coords + FIPS for a place. The polygon comes from TIGER. We hand back a unified GazetteerResult with shapely geometry, hiding the two-call shape from callers.

Useful when the consumer already has a FIPS lookup pipeline downstream (electoral / NCES / Decennial workflows) and would prefer to skip the WKLS / Nominatim translation step.

class siege_utilities.geo.gazetteers.census_gazetteer.CensusGazetteer[source]

Bases: object

US Census-backed gazetteer with admin polygons from TIGER.

Parameters:
  • timeout – Per-request HTTP timeout (seconds).

  • cache_size – LRU cache size for the (name, hints) key.

  • vintage (benchmark /) – Census Geocoder benchmark/vintage strings. Defaults pin to “current”; consumers stuck to a fixed vintage (e.g. for reproducibility against a published decennial roll) can override.

provider_name = 'census'
__init__(*, timeout=15.0, cache_size=1024, benchmark='Public_AR_Current', vintage='Current_Current')[source]
Parameters:
Return type:

None

is_available()[source]
Return type:

bool

lookup(name, *, country_hint=None, admin_hint=None)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • admin_hint (str | None)

Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

Nominatim (OSM) gazetteer backend.

Wraps the public OpenStreetMap Nominatim service via geopy. We ask for polygon_geojson=1 so the response carries admin polygons, not just point centroids — that’s what makes this useful as a Gazetteer.

Operational notes:

  • Public Nominatim has a 1 req/s rate limit and a Usage Policy that requires a meaningful user_agent. We pass one through configuration; callers running at scale should self-host.

  • server_url lets you point at a self-hosted Nominatim — when set, rate-limiting is dropped to a tiny delay.

  • In-process LRU cache wraps the per-name resolution. Nominatim does cache server-side, but the round-trip is hundreds of ms; in-process caching matters for interactive use.

class siege_utilities.geo.gazetteers.nominatim_gazetteer.NominatimGazetteer[source]

Bases: object

OpenStreetMap-backed gazetteer with polygon geometry.

Parameters:
  • user_agent – Required per OSM Usage Policy. Override the default for any production deployment so OSM ops can reach you.

  • server_urlNone for the public Nominatim (rate-limited at 1 req/s); otherwise a self-hosted Nominatim base URL.

  • timeout – Per-request timeout in seconds.

  • cache_size – LRU cache size for the (name, country, admin) key.

provider_name = 'nominatim'
__init__(*, user_agent='siege-utilities-gazetteer (https://github.com/siege-analytics/siege_utilities)', server_url=None, timeout=10.0, cache_size=1024)[source]
Parameters:
  • user_agent (str)

  • server_url (str | None)

  • timeout (float)

  • cache_size (int)

Return type:

None

is_available()[source]
Return type:

bool

lookup(name, *, country_hint=None, admin_hint=None)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • admin_hint (str | None)

Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

Wikidata + OSM gazetteer.

Two-step lookup:

  1. SPARQL query against Wikidata Query Service for an entity matching the place name. Pull wdt:P402 (OSM relation ID) and wdt:P625 (coordinate location) for each candidate.

  2. Deref the OSM relation ID against the Overpass API to get the admin polygon.

Useful for non-administrative places that WKLS / Nominatim’s admin boundary indexes miss: cultural regions, historical entities, informal areas. The fallback path (when P402 is absent) returns the P625 point with no polygon.

class siege_utilities.geo.gazetteers.wikidata_gazetteer.WikidataGazetteer[source]

Bases: object

Wikidata SPARQL + OSM Overpass gazetteer.

Parameters:
  • timeout – Per-request HTTP timeout.

  • cache_size – LRU cache for the (name, country) key.

  • overpass_url (sparql_url /) – Override for self-hosted instances.

provider_name = 'wikidata'
__init__(*, timeout=30.0, cache_size=1024, sparql_url='https://query.wikidata.org/sparql', overpass_url='https://overpass-api.de/api/interpreter')[source]
Parameters:
  • timeout (float)

  • cache_size (int)

  • sparql_url (str)

  • overpass_url (str)

Return type:

None

close()[source]

Close the underlying HTTP session.

Return type:

None

is_available()[source]
Return type:

bool

lookup(name, *, country_hint=None, admin_hint=None)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • admin_hint (str | None)

Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

WKLS (Well Known Locations) gazetteer backend.

Wraps the upstream wkls package to resolve place names against Overture Maps administrative boundaries (~625k globally). Why this is the default global backend:

  • No API key, no rate limit (reads from public S3 via embedded sedonadb).

  • sedonadb is Rust + Arrow-native — no JVM / Spark dependency, just a 302 MB install.

  • Source data is Overture Maps, the well-curated successor to patchwork-of-everything that older gazetteers carried.

Two siege_utilities-specific pieces this backend adds on top of the raw wkls API:

  1. Free-text → ISO code resolver. Etter outputs "Alabama"; wkls wants wkls.us.al. We use the wkls metadata search to map the human form into the chained-attribute form before calling the geometry getter.

  2. In-process LRU cache. The upstream library does not cache geometry fetches in-process — every .wkt() call hits S3 (~6s cold). For interactive use that’s a deal-breaker. We wrap the path (country, region, place_id) → shapely geometry with functools.lru_cache().

Verified against wkls==1.1.0 on 2026-05-07.

class siege_utilities.geo.gazetteers.wkls_gazetteer.WklsGazetteer[source]

Bases: object

WKLS-backed gazetteer with in-process LRU caching.

provider_name = 'wkls'
__init__(*, cache_size=1024)[source]
Parameters:

cache_size (int)

Return type:

None

is_available()[source]
Return type:

bool

lookup(name, *, country_hint=None, admin_hint=None)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • admin_hint (str | None)

Return type:

GazetteerResult

search(name, *, country_hint=None, limit=10)[source]
Parameters:
  • name (str)

  • country_hint (str | None)

  • limit (int)

Return type:

list[GazetteerCandidate]

Spatio-temporal place history query API.

Answers “what’s the story of this place over time?” by composing crosswalk chains and overlay data into a unified response.

Usage:

result = place_history("17031010100", from_year=2000, to_year=2020)
print(result.lineage)
print(result.overlays)

The overlay system is extensible — see overlay_registry for registering custom overlays.

class siege_utilities.geo.place_history.CrosswalkProvider[source]

Bases: ABC

Protocol for fetching crosswalk data.

Implementations provide crosswalk records for a given GEOID and vintage transition. The Django-backed provider queries TemporalCrosswalk; test providers use in-memory data.

abstractmethod get_forward_mappings(geoid, source_year, target_year, state_fips=None)[source]

Get target GEOIDs for a source GEOID transitioning between vintages.

Parameters:
  • geoid (str)

  • source_year (int)

  • target_year (int)

  • state_fips (str | None)

Return type:

list[CrosswalkRecord]

abstractmethod get_reverse_mappings(geoid, source_year, target_year, state_fips=None)[source]

Get source GEOIDs that map to a target GEOID.

Parameters:
  • geoid (str)

  • source_year (int)

  • target_year (int)

  • state_fips (str | None)

Return type:

list[CrosswalkRecord]

class siege_utilities.geo.place_history.CrosswalkRecord[source]

Bases: object

A single crosswalk mapping between two GEOIDs.

source_geoid: str
target_geoid: str
weight: float = 1.0
relationship: str = 'IDENTICAL'
__init__(source_geoid, target_geoid, weight=1.0, relationship='IDENTICAL')
Parameters:
  • source_geoid (str)

  • target_geoid (str)

  • weight (float)

  • relationship (str)

Return type:

None

class siege_utilities.geo.place_history.DictCrosswalkProvider[source]

Bases: CrosswalkProvider

In-memory crosswalk provider for testing.

Data is keyed by (source_year, target_year) → dict of source_geoid → list[CrosswalkRecord].

__init__(data=None)[source]
Parameters:

data (dict | None)

add(source_year, target_year, source_geoid, target_geoid, weight=1.0, relationship='IDENTICAL')[source]
Parameters:
  • source_year (int)

  • target_year (int)

  • source_geoid (str)

  • target_geoid (str)

  • weight (float)

  • relationship (str)

get_forward_mappings(geoid, source_year, target_year, state_fips=None)[source]

Get target GEOIDs for a source GEOID transitioning between vintages.

Parameters:
  • geoid (str)

  • source_year (int)

  • target_year (int)

  • state_fips (str | None)

Return type:

list[CrosswalkRecord]

get_reverse_mappings(geoid, source_year, target_year, state_fips=None)[source]

Get source GEOIDs that map to a target GEOID.

Parameters:
  • geoid (str)

  • source_year (int)

  • target_year (int)

  • state_fips (str | None)

Return type:

list[CrosswalkRecord]

class siege_utilities.geo.place_history.Lineage[source]

Bases: object

Complete crosswalk chain for a GEOID across time.

origin_geoid

Starting GEOID.

Type:

str

origin_year

Year of the starting GEOID.

Type:

int

steps

Ordered list of transitions.

Type:

list[siege_utilities.geo.place_history.LineageStep]

terminal_geoids

Final GEOID(s) at the end of the chain.

Type:

list[str]

direction

“forward” or “reverse”.

Type:

str

origin_geoid: str
origin_year: int
steps: list[LineageStep]
terminal_geoids: list[str]
direction: str = 'forward'
property is_unchanged: bool
__init__(origin_geoid, origin_year, steps=<factory>, terminal_geoids=<factory>, direction='forward')
Parameters:
Return type:

None

class siege_utilities.geo.place_history.LineageStep[source]

Bases: object

A single step in the crosswalk chain.

source_geoid

GEOID before transition.

Type:

str

target_geoid

GEOID after transition.

Type:

str

source_year

Source vintage year.

Type:

int

target_year

Target vintage year.

Type:

int

weight

Allocation weight (1.0 = identical).

Type:

float

relationship

IDENTICAL, SPLIT, MERGED, PARTIAL, RENAMED.

Type:

str

source_geoid: str
target_geoid: str
source_year: int
target_year: int
weight: float = 1.0
relationship: str = 'IDENTICAL'
__init__(source_geoid, target_geoid, source_year, target_year, weight=1.0, relationship='IDENTICAL')
Parameters:
  • source_geoid (str)

  • target_geoid (str)

  • source_year (int)

  • target_year (int)

  • weight (float)

  • relationship (str)

Return type:

None

class siege_utilities.geo.place_history.PlaceHistoryResult[source]

Bases: object

Unified response for a place history query.

geoid

The queried GEOID.

Type:

str

from_year

Start of the query range.

Type:

int

to_year

End of the query range.

Type:

int

lineage

Crosswalk chain showing boundary evolution.

Type:

siege_utilities.geo.place_history.Lineage | None

overlays

Dict of overlay name to overlay data.

Type:

dict[str, Any]

errors

Any errors encountered during the query.

Type:

list[str]

geoid: str
from_year: int
to_year: int
lineage: Lineage | None = None
overlays: dict[str, Any]
errors: list[str]
property has_lineage: bool
property direction: str
__init__(geoid, from_year, to_year, lineage=None, overlays=<factory>, errors=<factory>)
Parameters:
Return type:

None

siege_utilities.geo.place_history.place_history(geoid, from_year, to_year, overlays=None, state_fips=None, provider=None, overlay_registry=None)[source]

Query the history of a geographic place over time.

Composes crosswalk chains and optional overlays into a unified response showing how a place has evolved.

Parameters:
  • geoid (str) – Census GEOID to query (tract, block group, county, etc.).

  • from_year (int) – Start year of the query range.

  • to_year (int) – End year of the query range.

  • overlays (list[str] | None) – List of overlay names to include (e.g., [“seats”, “demographics”]).

  • state_fips (str | None) – Optional state FIPS code for filtering crosswalks.

  • provider (CrosswalkProvider | None) – CrosswalkProvider for fetching crosswalk data. If None, attempts to use the Django-backed provider.

  • overlay_registry (Any | None) – Registry for resolving overlay names to implementations. If None, overlays are skipped.

Returns:

PlaceHistoryResult with lineage and overlay data.

Return type:

PlaceHistoryResult

Overlays

Extensible overlay registry for place history queries.

Overlays add contextual data layers (seats, demographics, urbanicity, events) to place_history() results. New overlays plug in via the decorator-based registry without changing core query logic.

Usage:

from siege_utilities.geo.overlay_registry import (
    PlaceHistoryOverlay,
    overlay_registry,
)

@overlay_registry.register("my_overlay")
class MyOverlay(PlaceHistoryOverlay):
    def fetch(self, geoid, from_year, to_year, state_fips=None):
        return {"data": "..."}
class siege_utilities.geo.overlay_registry.OverlayRegistry[source]

Bases: object

Registry for place history overlays.

Supports both decorator-based and imperative registration.

Decorator usage:

@overlay_registry.register("seats")
class SeatsOverlay(PlaceHistoryOverlay):
    ...

Imperative usage:

overlay_registry.register_instance("seats", SeatsOverlay())
__init__()[source]
register(name)[source]

Decorator to register an overlay class.

The class is instantiated on first use (lazy).

Parameters:

name (str) – Overlay identifier used in place_history(overlays=[…]).

Returns:

Decorator that registers the class.

register_instance(name, instance)[source]

Register a pre-instantiated overlay.

Parameters:
get(name)[source]

Look up an overlay by name.

Lazily instantiates class-registered overlays on first access.

Parameters:

name (str) – Overlay identifier.

Returns:

PlaceHistoryOverlay instance, or None if not registered.

Raises:

Exception – If the registered overlay class fails to instantiate.

Return type:

PlaceHistoryOverlay | None

list_overlays()[source]

Return names of all registered overlays.

Return type:

list[str]

is_registered(name)[source]

Check if an overlay name is registered.

Parameters:

name (str)

Return type:

bool

unregister(name)[source]

Remove an overlay registration.

Parameters:

name (str) – Overlay identifier.

Returns:

True if the overlay was found and removed.

Return type:

bool

clear()[source]

Remove all registrations.

class siege_utilities.geo.overlay_registry.PlaceHistoryOverlay[source]

Bases: ABC

Base class for place history overlays.

Subclasses implement fetch() to retrieve overlay-specific data for a queried GEOID and time range.

abstract property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

abstractmethod fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

Any

is_available()[source]

Check if this overlay’s data source is accessible.

Return type:

bool

Place history overlay implementations.

Demographics overlay for place history.

Assembles DemographicSnapshot time series for a queried geography, handling GEOID changes via the crosswalk chain from the lineage.

class siege_utilities.geo.overlays.demographics.DemographicPoint[source]

Bases: object

A single demographic data point for a year.

year

Data year.

Type:

int

geoid

GEOID this data belongs to.

Type:

str

dataset

Dataset identifier (acs5, acs1, dec, dec_pl).

Type:

str

total_population

Total population if available.

Type:

int | None

median_household_income

Median HH income if available.

Type:

float | None

median_age

Median age if available.

Type:

float | None

values

Full variable code → value mapping.

Type:

dict[str, Any]

year: int = 0
geoid: str = ''
dataset: str = ''
total_population: int | None = None
median_household_income: float | None = None
median_age: float | None = None
values: dict[str, Any]
__init__(year=0, geoid='', dataset='', total_population=None, median_household_income=None, median_age=None, values=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.overlays.demographics.DemographicsDataProvider[source]

Bases: ABC

Protocol for fetching demographic snapshot data.

abstractmethod get_snapshots(geoid, from_year, to_year, dataset=None)[source]

Get demographic data points for a GEOID and year range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • dataset (str | None)

Return type:

list[DemographicPoint]

class siege_utilities.geo.overlays.demographics.DemographicsOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay for demographic time series.

Queries DemographicSnapshot data for the queried GEOID and any related GEOIDs from the crosswalk chain (when a GEOID changes across decades).

Parameters:
  • provider – DemographicsDataProvider for fetching snapshot data.

  • dataset – Restrict to a specific dataset (e.g., “acs5”).

  • terminal_geoids – Additional GEOIDs to query (from lineage).

__init__(provider=None, dataset=None)[source]
Parameters:
property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None, terminal_geoids=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

  • terminal_geoids (list[str] | None)

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

DemographicsOverlayResult

class siege_utilities.geo.overlays.demographics.DemographicsOverlayResult[source]

Bases: object

Result of the demographics overlay query.

geoid

Queried GEOID.

Type:

str

time_series

Ordered list of demographic data points.

Type:

list[siege_utilities.geo.overlays.demographics.DemographicPoint]

geoids_queried

All GEOIDs queried (includes crosswalk targets).

Type:

list[str]

geoid: str = ''
time_series: list[DemographicPoint]
geoids_queried: list[str]
property years: list[int]
property has_data: bool
for_year(year)[source]
Parameters:

year (int)

Return type:

list[DemographicPoint]

population_series()[source]
Return type:

list[tuple[int, int | None]]

__init__(geoid='', time_series=<factory>, geoids_queried=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.overlays.demographics.DictDemographicsProvider[source]

Bases: DemographicsDataProvider

In-memory demographics provider for testing.

__init__()[source]
add(point)[source]
Parameters:

point (DemographicPoint)

get_snapshots(geoid, from_year, to_year, dataset=None)[source]

Get demographic data points for a GEOID and year range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • dataset (str | None)

Return type:

list[DemographicPoint]

Election results overlay for place history.

Shows precinct-level election returns for a queried geography, reconciled via precinct-VTD mapping where needed.

class siege_utilities.geo.overlays.election_results.DictElectionDataProvider[source]

Bases: ElectionDataProvider

In-memory election data provider for testing.

__init__()[source]
add(ret)[source]
Parameters:

ret (ElectionReturn)

get_election_returns(geoid, from_year, to_year, state_fips=None)[source]

Get election returns for precincts/VTDs overlapping a geography.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[ElectionReturn]

class siege_utilities.geo.overlays.election_results.ElectionDataProvider[source]

Bases: ABC

Protocol for fetching election results for a geography.

abstractmethod get_election_returns(geoid, from_year, to_year, state_fips=None)[source]

Get election returns for precincts/VTDs overlapping a geography.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[ElectionReturn]

class siege_utilities.geo.overlays.election_results.ElectionResultsOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay for precinct-level election results.

Returns election returns for precincts/VTDs that overlap the queried geography, reconciled via precinct-VTD mapping.

__init__(provider=None)[source]
Parameters:

provider (ElectionDataProvider | None)

property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

ElectionResultsOverlayResult

is_available()[source]

Check if this overlay’s data source is accessible.

Return type:

bool

class siege_utilities.geo.overlays.election_results.ElectionResultsOverlayResult[source]

Bases: object

Result of the election results overlay query.

geoid

Queried GEOID.

Type:

str

returns

Election returns ordered by year then office.

Type:

list[siege_utilities.geo.overlays.election_results.ElectionReturn]

election_years

Distinct years with data.

offices

Distinct offices with data.

geoid: str = ''
returns: list[ElectionReturn]
property election_years: list[int]
property offices: list[str]
for_year(year)[source]
Parameters:

year (int)

Return type:

list[ElectionReturn]

for_office(office)[source]
Parameters:

office (str)

Return type:

list[ElectionReturn]

for_year_office(year, office)[source]
Parameters:
Return type:

list[ElectionReturn]

party_totals(year, office)[source]

Sum votes by party for a specific contest.

Parameters:
Return type:

dict[str, int]

__init__(geoid='', returns=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.overlays.election_results.ElectionReturn[source]

Bases: object

Election result for a single contest in a single precinct/VTD.

precinct_id

Precinct or VTD identifier.

Type:

str

election_year

Year of the election.

Type:

int

election_type

general, primary, runoff, special.

Type:

str

office

Office contested (e.g., US_PRESIDENT, US_HOUSE, GOVERNOR).

Type:

str

candidate

Candidate name.

Type:

str

party

Party abbreviation (DEM, REP, LIB, etc.).

Type:

str

votes

Vote count.

Type:

int

total_votes

Total votes cast in this contest at this precinct.

Type:

int

vote_share

Fraction of total votes (0-1).

Type:

float

state

State abbreviation.

Type:

str

reconciliation_method

How this precinct was mapped (spatial, name_match, official_crosswalk, or direct).

Type:

str

precinct_id: str = ''
election_year: int = 0
election_type: str = 'general'
office: str = ''
candidate: str = ''
party: str = ''
votes: int = 0
total_votes: int = 0
vote_share: float = 0.0
state: str = ''
reconciliation_method: str = 'direct'
__init__(precinct_id='', election_year=0, election_type='general', office='', candidate='', party='', votes=0, total_votes=0, vote_share=0.0, state='', reconciliation_method='direct')
Parameters:
  • precinct_id (str)

  • election_year (int)

  • election_type (str)

  • office (str)

  • candidate (str)

  • party (str)

  • votes (int)

  • total_votes (int)

  • vote_share (float)

  • state (str)

  • reconciliation_method (str)

Return type:

None

Events overlay for place history.

SpatioTemporalEvents intersecting the queried geography and time range. Events include court rulings, natural disasters, redistricting actions, and other discrete occurrences with both spatial and temporal extent.

class siege_utilities.geo.overlays.events.DictEventsProvider[source]

Bases: EventsDataProvider

In-memory events provider for testing.

__init__()[source]
add(event)[source]
Parameters:

event (EventRecord)

get_events(geoid, from_year, to_year, event_types=None)[source]

Get events intersecting a GEOID within a time range.

Parameters:
Return type:

list[EventRecord]

class siege_utilities.geo.overlays.events.EventRecord[source]

Bases: object

A single spatio-temporal event.

event_id

Unique identifier.

Type:

str

event_type

Category (e.g., “court_ruling”, “natural_disaster”, “redistricting”, “election”, “policy_change”).

Type:

str

title

Short description.

Type:

str

description

Longer description.

Type:

str

date

Event date.

Type:

datetime.date | None

end_date

End date for events with duration.

Type:

datetime.date | None

geoids_affected

GEOIDs intersecting this event’s geography.

Type:

list[str]

source

Data source or citation.

Type:

str

metadata

Additional event-specific data.

Type:

dict[str, Any]

event_id: str = ''
event_type: str = ''
title: str = ''
description: str = ''
date: date | None = None
end_date: date | None = None
geoids_affected: list[str]
source: str = ''
metadata: dict[str, Any]
property year: int
property has_duration: bool
__init__(event_id='', event_type='', title='', description='', date=None, end_date=None, geoids_affected=<factory>, source='', metadata=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.overlays.events.EventsDataProvider[source]

Bases: ABC

Protocol for fetching spatio-temporal event data.

abstractmethod get_events(geoid, from_year, to_year, event_types=None)[source]

Get events intersecting a GEOID within a time range.

Parameters:
Return type:

list[EventRecord]

class siege_utilities.geo.overlays.events.EventsOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay for spatio-temporal events.

Finds events that intersect the queried geography and time range.

Parameters:
  • provider – EventsDataProvider for fetching events.

  • event_types – Optional filter for specific event types.

__init__(provider=None, event_types=None)[source]
Parameters:
property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

EventsOverlayResult

class siege_utilities.geo.overlays.events.EventsOverlayResult[source]

Bases: object

Result of the events overlay query.

geoid

Queried GEOID.

Type:

str

events

Events intersecting the geography and time range.

Type:

list[siege_utilities.geo.overlays.events.EventRecord]

geoid: str = ''
events: list[EventRecord]
property has_events: bool
property event_types: list[str]
for_type(event_type)[source]
Parameters:

event_type (str)

Return type:

list[EventRecord]

property count: int
__init__(geoid='', events=<factory>)
Parameters:
Return type:

None

Redistricting overlay for place history.

Shows which redistricting plan(s) and district(s) applied to a geography over a queried time range, using plan lifecycle data.

class siege_utilities.geo.overlays.redistricting.DictRedistrictingDataProvider[source]

Bases: RedistrictingDataProvider

In-memory redistricting data provider for testing.

__init__()[source]
add(assignment)[source]
Parameters:

assignment (DistrictAssignment)

get_district_assignments(geoid, from_year, to_year, state_fips=None)[source]

Get district assignments for a geography over a time range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[DistrictAssignment]

class siege_utilities.geo.overlays.redistricting.DistrictAssignment[source]

Bases: object

A district assignment for a geography under a specific plan.

plan_id

Plan identifier.

Type:

str

plan_name

Human-readable plan name.

Type:

str

plan_type

Congressional, state senate, or state house.

Type:

str

district_id

District identifier (e.g., “TX-28”).

Type:

str

state

State abbreviation.

Type:

str

from_date

When this assignment began.

Type:

datetime.date | None

to_date

When this assignment ended (None = still active).

Type:

datetime.date | None

enacted_by

Who enacted the plan.

Type:

str

court_case

Court case if court-ordered.

Type:

str

containment_pct

Fraction of queried geography in this district.

Type:

float

plan_id: str = ''
plan_name: str = ''
plan_type: str = ''
district_id: str = ''
state: str = ''
from_date: date | None = None
to_date: date | None = None
enacted_by: str = ''
court_case: str = ''
containment_pct: float = 1.0
__init__(plan_id='', plan_name='', plan_type='', district_id='', state='', from_date=None, to_date=None, enacted_by='', court_case='', containment_pct=1.0)
Parameters:
  • plan_id (str)

  • plan_name (str)

  • plan_type (str)

  • district_id (str)

  • state (str)

  • from_date (date | None)

  • to_date (date | None)

  • enacted_by (str)

  • court_case (str)

  • containment_pct (float)

Return type:

None

class siege_utilities.geo.overlays.redistricting.RedistrictingDataProvider[source]

Bases: ABC

Protocol for fetching redistricting plan assignments for a geography.

abstractmethod get_district_assignments(geoid, from_year, to_year, state_fips=None)[source]

Get district assignments for a geography over a time range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[DistrictAssignment]

class siege_utilities.geo.overlays.redistricting.RedistrictingOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay for redistricting plan assignments.

Shows which plan(s) and district(s) applied to a geography over the queried time range.

__init__(provider=None)[source]
Parameters:

provider (RedistrictingDataProvider | None)

property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

RedistrictingOverlayResult

is_available()[source]

Check if this overlay’s data source is accessible.

Return type:

bool

class siege_utilities.geo.overlays.redistricting.RedistrictingOverlayResult[source]

Bases: object

Result of the redistricting overlay query.

geoid

Queried GEOID.

Type:

str

assignments

District assignments ordered by time.

Type:

list[siege_utilities.geo.overlays.redistricting.DistrictAssignment]

plan_count

Number of distinct plans that applied.

had_court_intervention

Whether any plan was court-ordered.

geoid: str = ''
assignments: list[DistrictAssignment]
property plan_count: int
property had_court_intervention: bool
property plan_types: list[str]
for_plan_type(plan_type)[source]
Parameters:

plan_type (str)

Return type:

list[DistrictAssignment]

active_at(when)[source]
Parameters:

when (date)

Return type:

list[DistrictAssignment]

__init__(geoid='', assignments=<factory>)
Parameters:
Return type:

None

Seats overlay for place history.

Shows which congressional/state legislative districts contained a geography over time, under which redistricting plans.

Key design constraint from the ST model: “Seat is identity, not geography.” The overlay returns Seat + Plan, not raw CD geometry.

class siege_utilities.geo.overlays.seats.DictSeatsProvider[source]

Bases: SeatsDataProvider

In-memory seats provider for testing.

__init__()[source]
add(assignment)[source]
Parameters:

assignment (SeatAssignment)

get_assignments(geoid, from_year, to_year, state_fips=None)[source]

Get seat assignments for a GEOID and time range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[SeatAssignment]

class siege_utilities.geo.overlays.seats.SeatAssignment[source]

Bases: object

A seat-to-geography assignment for a time period.

seat_label

Human-readable seat label (e.g., “US House CA-12”).

Type:

str

office

Office type (US_HOUSE, US_SENATE, STATE_UPPER, STATE_LOWER).

Type:

str

district_label

District number/label.

Type:

str

state_fips

State FIPS code.

Type:

str

plan_name

Name of the redistricting plan.

Type:

str

plan_year

Year the plan took effect.

Type:

int | None

from_year

Start year of this assignment.

Type:

int | None

to_year

End year of this assignment.

Type:

int | None

containment_pct

Fraction of the queried geography in this district.

Type:

float

seat_label: str = ''
office: str = ''
district_label: str = ''
state_fips: str = ''
plan_name: str = ''
plan_year: int | None = None
from_year: int | None = None
to_year: int | None = None
containment_pct: float = 1.0
__init__(seat_label='', office='', district_label='', state_fips='', plan_name='', plan_year=None, from_year=None, to_year=None, containment_pct=1.0)
Parameters:
  • seat_label (str)

  • office (str)

  • district_label (str)

  • state_fips (str)

  • plan_name (str)

  • plan_year (int | None)

  • from_year (int | None)

  • to_year (int | None)

  • containment_pct (float)

Return type:

None

class siege_utilities.geo.overlays.seats.SeatsDataProvider[source]

Bases: ABC

Protocol for fetching seat assignment data.

Implementations query PlanDistrictAssignment + BoundaryIntersection to find which districts contained a GEOID over time.

abstractmethod get_assignments(geoid, from_year, to_year, state_fips=None)[source]

Get seat assignments for a GEOID and time range.

Parameters:
  • geoid (str)

  • from_year (int)

  • to_year (int)

  • state_fips (str | None)

Return type:

list[SeatAssignment]

class siege_utilities.geo.overlays.seats.SeatsOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay showing district/seat assignments over time.

Parameters:

provider – SeatsDataProvider for fetching assignment data. If None, attempts to use the Django-backed provider.

__init__(provider=None)[source]
Parameters:

provider (SeatsDataProvider | None)

property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

SeatsOverlayResult

class siege_utilities.geo.overlays.seats.SeatsOverlayResult[source]

Bases: object

Result of the seats overlay query.

geoid

Queried GEOID.

Type:

str

assignments

Seat assignments ordered by time.

Type:

list[siege_utilities.geo.overlays.seats.SeatAssignment]

current_districts

Districts containing the GEOID now.

geoid: str = ''
assignments: list[SeatAssignment]
property current_districts: list[SeatAssignment]
property offices: list[str]
for_office(office)[source]
Parameters:

office (str)

Return type:

list[SeatAssignment]

__init__(geoid='', assignments=<factory>)
Parameters:
Return type:

None

Urbanicity overlay for place history.

NCES locale classification per vintage for a queried geography. Calls NCESLocaleClassifier (WS3) for each vintage in the range.

class siege_utilities.geo.overlays.urbanicity.DictUrbanicityProvider[source]

Bases: UrbanicityDataProvider

In-memory urbanicity provider for testing.

__init__()[source]
add(point)[source]
Parameters:

point (UrbanicityPoint)

classify(geoid, year)[source]

Classify a GEOID for a given vintage year.

Parameters:
Return type:

UrbanicityPoint | None

class siege_utilities.geo.overlays.urbanicity.UrbanicityDataProvider[source]

Bases: ABC

Protocol for fetching NCES locale classification data.

abstractmethod classify(geoid, year)[source]

Classify a GEOID for a given vintage year.

Parameters:
Return type:

UrbanicityPoint | None

class siege_utilities.geo.overlays.urbanicity.UrbanicityOverlay[source]

Bases: PlaceHistoryOverlay

Place history overlay for NCES urbanicity classification.

Classifies the queried GEOID using the NCES 12-code locale system for each relevant vintage in the query range.

Parameters:
  • provider – UrbanicityDataProvider for classification.

  • vintage_years – Years to check. Defaults to NCES vintages.

__init__(provider=None, vintage_years=None)[source]
Parameters:
property name: str

Short identifier for this overlay (e.g., ‘seats’, ‘demographics’).

fetch(geoid, from_year, to_year, state_fips=None)[source]

Fetch overlay data for a place and time range.

Parameters:
  • geoid (str) – Census GEOID to query.

  • from_year (int) – Start year.

  • to_year (int) – End year.

  • state_fips (str | None) – Optional state FIPS for filtering.

Returns:

Overlay-specific data (dict, list, dataclass, etc.).

Return type:

UrbanicityOverlayResult

class siege_utilities.geo.overlays.urbanicity.UrbanicityOverlayResult[source]

Bases: object

Result of the urbanicity overlay query.

geoid

Queried GEOID.

Type:

str

classifications

Ordered list of urbanicity points by year.

Type:

list[siege_utilities.geo.overlays.urbanicity.UrbanicityPoint]

geoid: str = ''
classifications: list[UrbanicityPoint]
property years: list[int]
property has_data: bool
for_year(year)[source]
Parameters:

year (int)

Return type:

UrbanicityPoint | None

property changed: bool
property current_category: str
__init__(geoid='', classifications=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.overlays.urbanicity.UrbanicityPoint[source]

Bases: object

NCES locale classification for a single vintage.

year

Vintage year of the classification.

Type:

int

geoid

GEOID classified.

Type:

str

locale_code

NCES 2-digit locale code (e.g., “11” for City-Large).

Type:

str

locale_label

Human-readable label (e.g., “City-Large”).

Type:

str

category

Top-level category (city, suburb, town, rural).

Type:

str

size

Size qualifier (large, midsize, small, fringe, distant, remote).

Type:

str

year: int = 0
geoid: str = ''
locale_code: str = ''
locale_label: str = ''
category: str = ''
size: str = ''
__init__(year=0, geoid='', locale_code='', locale_label='', category='', size='')
Parameters:
Return type:

None

Redistricting and Plans

Redistricting plan resolution by date (issue #361).

Models a redistricting plan as a named, dated artifact and exposes a PlanRegistry that resolves the plan in effect for a (state_fips, district_type, date) tuple. Replaces the decade-keyed CensusVintageConfig for callers that need to handle mid-cycle court orders (Alabama 2023, Louisiana 2024, etc.).

This is the foundational module — it has no consumer code yet. Downstream PRs wire RDHProvider and assign_boundaries to consult the registry.

class siege_utilities.geo.plans.PlanAuthority[source]

Bases: str, Enum

Who drew the plan — legally meaningful for citation and audit.

LEGISLATURE = 'legislature'

Enacted by the state legislature (the default cycle path).

COURT = 'court'

Court-imposed map (interim or final).

COMMISSION = 'commission'

Independent or politician redistricting commission.

DEFAULT = 'default'

Pre-decennial-shift baseline; used by the legacy resolver when no explicit plan covers a date.

__new__(value)
class siege_utilities.geo.plans.PlanDistrict[source]

Bases: object

A single district within a redistricting plan.

All boundaries fields are optional — many consumers only need the identity (which plan, which district, valid when) and resolve the geometry separately via a BoundaryProvider.

state_fips

2-character state FIPS code (e.g. "01" for Alabama).

Type:

str

district_type

Lowercase district class — "cd", "sldu", "sldl", "county_commission", etc. Match the keys used by CensusTIGERProvider.

Type:

str

district_id

District identifier within the plan. "7" for AL-7, "01" for SLDU 1, etc. Stored as the string form so leading-zero district numbers (sometimes used by states) are preserved.

Type:

str

plan_name

Stable, human-readable plan identifier (e.g. "AL_2023_CD_INTERIM"). Conventional but not enforced.

Type:

str

authority

Who drew the plan (see PlanAuthority).

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the district is in legal effect.

Type:

datetime.date

effective_to

Last date the district is in legal effect, or ``None`` for an open-ended plan (the currently active one).

Type:

datetime.date | None

geometry_source

Optional pointer to the geometry — a URL, a local path, an RDH dataset slug, or any string the consuming BoundaryProvider understands. None is fine if callers only need identity resolution.

Type:

str | None

notes

Free-text annotation (e.g. court docket, statute citation). Goes through to logs and audit trails verbatim.

Type:

str | None

state_fips: str
district_type: str
district_id: str
plan_name: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
geometry_source: str | None = None
notes: str | None = None
covers_date(when)[source]

Return True iff when falls within this district’s effective span.

Half-open at the upper end: effective_to is inclusive (if the new plan starts the next day, encode that as effective_from = old_to + timedelta(days=1)). This matches how court orders are typically written (“effective for elections on or after DATE”) and avoids the off-by-one trap of half-open Python slices.

Parameters:

when (date)

Return type:

bool

__init__(state_fips, district_type, district_id, plan_name, authority, effective_from, effective_to=None, geometry_source=None, notes=None)
Parameters:
  • state_fips (str)

  • district_type (str)

  • district_id (str)

  • plan_name (str)

  • authority (PlanAuthority)

  • effective_from (date)

  • effective_to (date | None)

  • geometry_source (str | None)

  • notes (str | None)

Return type:

None

class siege_utilities.geo.plans.RedistrictingPlan[source]

Bases: object

A full plan: a collection of districts plus plan-level metadata.

The plan is the unit of court orders, statutes, and commission actions; the individual PlanDistrict rows just decompose it for query convenience. effective_from / effective_to on the plan should match the values on its constituent districts.

plan_name

Same convention as PlanDistrict.plan_name.

Type:

str

state_fips

2-character state FIPS.

Type:

str

district_type

"cd", "sldu", etc.

Type:

str

authority

Who drew it.

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the plan is in legal effect.

Type:

datetime.date

effective_to

Last date in effect, or None if currently active.

Type:

datetime.date | None

districts

Tuple of PlanDistrict rows under this plan.

Type:

Tuple[siege_utilities.geo.plans.models.PlanDistrict, …]

metadata

Free-form mapping for citation, source URLs, court docket numbers, etc. Not used for resolution — consumers can stuff whatever they want here.

Type:

Mapping[str, str]

Note: frozen=True only forbids attribute reassignment; the metadata dict remains mutable in place. Instances are therefore not hashable and should be treated as value objects, never used as dict keys or set members.

plan_name: str
state_fips: str
district_type: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
districts: Tuple[PlanDistrict, ...]
metadata: Mapping[str, str]
covers_date(when)[source]

Return True iff when falls within this plan’s effective span.

Parameters:

when (date)

Return type:

bool

district(district_id)[source]

Find a district within this plan by id, or None.

Parameters:

district_id (str)

Return type:

PlanDistrict | None

__init__(plan_name, state_fips, district_type, authority, effective_from, effective_to=None, districts=<factory>, metadata=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.plans.PlanRegistry[source]

Bases: object

Stores redistricting plans and resolves them by date.

Plans are keyed by (state_fips, district_type). Within a key, plans should be temporally non-overlapping; the registry detects overlap at registration time (warn) and at resolution time (raise in strict mode).

Thread-safe for concurrent readers; mutation under a lock.

__init__()[source]
Return type:

None

register_plan(plan)[source]

Add plan to the registry.

Logs a warning if it overlaps an already-registered plan for the same (state_fips, district_type). Does not raise on overlap — registration is forgiving so consumers can load partial data; resolution is the strict gate.

Parameters:

plan (RedistrictingPlan)

Return type:

None

register_plans(plans)[source]

Bulk-register an iterable of plans.

Parameters:

plans (Iterable[RedistrictingPlan])

Return type:

None

clear()[source]

Remove all registered plans (mostly for tests).

Return type:

None

plans_for_state(state_fips, district_type)[source]

Return all registered plans for state_fips / district_type.

Sorted by effective_from ascending. Empty list if none.

Parameters:
  • state_fips (str)

  • district_type (str)

Return type:

List[RedistrictingPlan]

resolve_plan_at_date(state_fips, district_type, when, *, strict=True)[source]

Find the plan in effect for state_fips / district_type on when.

Parameters:
  • state_fips (str) – 2-char state FIPS.

  • district_type (str) – "cd", "sldu", "sldl", etc.

  • when (date) – The date to resolve.

  • strict (bool) – If True (default), raise PlanOverlapError when more than one plan covers when. If False, log a warning and return the most recently enacted one (latest effective_from) — appropriate when court interim/final pairs both technically cover the same day.

Raises:
Return type:

RedistrictingPlan

resolve_district_at_date(state_fips, district_type, district_id, when, *, strict=True)[source]

Find the specific district covering when.

Convenience wrapper: resolves the plan, then looks up the district by id. Raises PlanResolutionError if the plan does not contain district_id.

Parameters:
Return type:

PlanDistrict

exception siege_utilities.geo.plans.PlanResolutionError[source]

Bases: LookupError

No plan covers the requested (state, district_type, date) tuple.

exception siege_utilities.geo.plans.PlanOverlapError[source]

Bases: ValueError

Two registered plans cover the same date for the same state+type.

Raised by PlanRegistry.resolve_plan_at_date() when the registry finds more than one match in strict mode. Caller can rerun with strict=False to get the first match (and a warning), or fix the underlying plan data.

siege_utilities.geo.plans.get_default_plan_registry()[source]

Return the process-global default PlanRegistry.

Lazily instantiated on first call. Consumers that need isolation (multi-tenant code, unit tests) should instantiate PlanRegistry directly instead.

Return type:

PlanRegistry

Data models for redistricting plans and their districts.

A plan is a named, dated cartographic artifact: e.g. “Alabama 2023 court-interim congressional map”. Plans replace each other over time — the same state has multiple plans for the same district type within a single decennial cycle when a court intervenes (Alabama 2022/2023, Louisiana 2023/2024, New York 2022/2024, Georgia 2023/2024).

The existing CensusVintageConfig in this package keys boundaries by decade (“2010 → 2010-2019”), which cannot represent a donation made in AL-7 in March 2023 vs the same address in September 2023 (different geometry). These models give consumers a date-resolvable shape.

This module is the foundation of the redistricting-plan resolution system (issue #361). The matching siege_utilities.geo.plans.registry provides date-based lookup; downstream PRs wire RDHProvider and assign_boundaries to use it.

class siege_utilities.geo.plans.models.PlanAuthority[source]

Bases: str, Enum

Who drew the plan — legally meaningful for citation and audit.

LEGISLATURE = 'legislature'

Enacted by the state legislature (the default cycle path).

COURT = 'court'

Court-imposed map (interim or final).

COMMISSION = 'commission'

Independent or politician redistricting commission.

DEFAULT = 'default'

Pre-decennial-shift baseline; used by the legacy resolver when no explicit plan covers a date.

__new__(value)
class siege_utilities.geo.plans.models.PlanDistrict[source]

Bases: object

A single district within a redistricting plan.

All boundaries fields are optional — many consumers only need the identity (which plan, which district, valid when) and resolve the geometry separately via a BoundaryProvider.

state_fips

2-character state FIPS code (e.g. "01" for Alabama).

Type:

str

district_type

Lowercase district class — "cd", "sldu", "sldl", "county_commission", etc. Match the keys used by CensusTIGERProvider.

Type:

str

district_id

District identifier within the plan. "7" for AL-7, "01" for SLDU 1, etc. Stored as the string form so leading-zero district numbers (sometimes used by states) are preserved.

Type:

str

plan_name

Stable, human-readable plan identifier (e.g. "AL_2023_CD_INTERIM"). Conventional but not enforced.

Type:

str

authority

Who drew the plan (see PlanAuthority).

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the district is in legal effect.

Type:

datetime.date

effective_to

Last date the district is in legal effect, or ``None`` for an open-ended plan (the currently active one).

Type:

datetime.date | None

geometry_source

Optional pointer to the geometry — a URL, a local path, an RDH dataset slug, or any string the consuming BoundaryProvider understands. None is fine if callers only need identity resolution.

Type:

str | None

notes

Free-text annotation (e.g. court docket, statute citation). Goes through to logs and audit trails verbatim.

Type:

str | None

state_fips: str
district_type: str
district_id: str
plan_name: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
geometry_source: str | None = None
notes: str | None = None
covers_date(when)[source]

Return True iff when falls within this district’s effective span.

Half-open at the upper end: effective_to is inclusive (if the new plan starts the next day, encode that as effective_from = old_to + timedelta(days=1)). This matches how court orders are typically written (“effective for elections on or after DATE”) and avoids the off-by-one trap of half-open Python slices.

Parameters:

when (date)

Return type:

bool

__init__(state_fips, district_type, district_id, plan_name, authority, effective_from, effective_to=None, geometry_source=None, notes=None)
Parameters:
  • state_fips (str)

  • district_type (str)

  • district_id (str)

  • plan_name (str)

  • authority (PlanAuthority)

  • effective_from (date)

  • effective_to (date | None)

  • geometry_source (str | None)

  • notes (str | None)

Return type:

None

class siege_utilities.geo.plans.models.RedistrictingPlan[source]

Bases: object

A full plan: a collection of districts plus plan-level metadata.

The plan is the unit of court orders, statutes, and commission actions; the individual PlanDistrict rows just decompose it for query convenience. effective_from / effective_to on the plan should match the values on its constituent districts.

plan_name

Same convention as PlanDistrict.plan_name.

Type:

str

state_fips

2-character state FIPS.

Type:

str

district_type

"cd", "sldu", etc.

Type:

str

authority

Who drew it.

Type:

siege_utilities.geo.plans.models.PlanAuthority

effective_from

First date the plan is in legal effect.

Type:

datetime.date

effective_to

Last date in effect, or None if currently active.

Type:

datetime.date | None

districts

Tuple of PlanDistrict rows under this plan.

Type:

Tuple[siege_utilities.geo.plans.models.PlanDistrict, …]

metadata

Free-form mapping for citation, source URLs, court docket numbers, etc. Not used for resolution — consumers can stuff whatever they want here.

Type:

Mapping[str, str]

Note: frozen=True only forbids attribute reassignment; the metadata dict remains mutable in place. Instances are therefore not hashable and should be treated as value objects, never used as dict keys or set members.

plan_name: str
state_fips: str
district_type: str
authority: PlanAuthority
effective_from: date
effective_to: date | None = None
districts: Tuple[PlanDistrict, ...]
metadata: Mapping[str, str]
covers_date(when)[source]

Return True iff when falls within this plan’s effective span.

Parameters:

when (date)

Return type:

bool

district(district_id)[source]

Find a district within this plan by id, or None.

Parameters:

district_id (str)

Return type:

PlanDistrict | None

__init__(plan_name, state_fips, district_type, authority, effective_from, effective_to=None, districts=<factory>, metadata=<factory>)
Parameters:
Return type:

None

Date-resolvable registry of redistricting plans.

Consumers register RedistrictingPlan instances and then ask “which plan was active in Alabama for congressional districts on 2023-09-15?” — the registry returns the right plan (or raises if there is overlap or a gap, depending on strictness).

The registry is in-memory by design. File-backed plan catalogs (RDH manifest, NCSL plan registry) are loaded into an instance via PlanRegistry.register_plan() at consumer startup; the resolver itself doesn’t know about disk.

A module-level singleton (get_default_plan_registry()) is provided for the common case where one process has one global view of plans. Consumers that need isolation (multi-tenant, tests) instantiate their own.

class siege_utilities.geo.plans.registry.PlanRegistry[source]

Bases: object

Stores redistricting plans and resolves them by date.

Plans are keyed by (state_fips, district_type). Within a key, plans should be temporally non-overlapping; the registry detects overlap at registration time (warn) and at resolution time (raise in strict mode).

Thread-safe for concurrent readers; mutation under a lock.

__init__()[source]
Return type:

None

register_plan(plan)[source]

Add plan to the registry.

Logs a warning if it overlaps an already-registered plan for the same (state_fips, district_type). Does not raise on overlap — registration is forgiving so consumers can load partial data; resolution is the strict gate.

Parameters:

plan (RedistrictingPlan)

Return type:

None

register_plans(plans)[source]

Bulk-register an iterable of plans.

Parameters:

plans (Iterable[RedistrictingPlan])

Return type:

None

clear()[source]

Remove all registered plans (mostly for tests).

Return type:

None

plans_for_state(state_fips, district_type)[source]

Return all registered plans for state_fips / district_type.

Sorted by effective_from ascending. Empty list if none.

Parameters:
  • state_fips (str)

  • district_type (str)

Return type:

List[RedistrictingPlan]

resolve_plan_at_date(state_fips, district_type, when, *, strict=True)[source]

Find the plan in effect for state_fips / district_type on when.

Parameters:
  • state_fips (str) – 2-char state FIPS.

  • district_type (str) – "cd", "sldu", "sldl", etc.

  • when (date) – The date to resolve.

  • strict (bool) – If True (default), raise PlanOverlapError when more than one plan covers when. If False, log a warning and return the most recently enacted one (latest effective_from) — appropriate when court interim/final pairs both technically cover the same day.

Raises:
Return type:

RedistrictingPlan

resolve_district_at_date(state_fips, district_type, district_id, when, *, strict=True)[source]

Find the specific district covering when.

Convenience wrapper: resolves the plan, then looks up the district by id. Raises PlanResolutionError if the plan does not contain district_id.

Parameters:
Return type:

PlanDistrict

exception siege_utilities.geo.plans.registry.PlanResolutionError[source]

Bases: LookupError

No plan covers the requested (state, district_type, date) tuple.

exception siege_utilities.geo.plans.registry.PlanOverlapError[source]

Bases: ValueError

Two registered plans cover the same date for the same state+type.

Raised by PlanRegistry.resolve_plan_at_date() when the registry finds more than one match in strict mode. Caller can rerun with strict=False to get the first match (and a warning), or fix the underlying plan data.

siege_utilities.geo.plans.registry.get_default_plan_registry()[source]

Return the process-global default PlanRegistry.

Lazily instantiated on first call. Consumers that need isolation (multi-tenant code, unit tests) should instantiate PlanRegistry directly instead.

Return type:

PlanRegistry

Redistricting plan lifecycle tracking.

Models the lifecycle of redistricting plans: proposed -> adopted -> enacted -> challenged -> stayed/struck -> superseded. Supports court-order branching and temporal queries (“what plan was in effect for TX congress on 2023-09-15?”).

class siege_utilities.geo.plan_lifecycle.DictPlanLifecycleProvider[source]

Bases: PlanLifecycleProvider

In-memory plan lifecycle provider for testing.

__init__()[source]
add(plan)[source]
Parameters:

plan (RedistrictingPlan)

get_plans(state, plan_type=None)[source]

Get all plans for a state, optionally filtered by type.

Parameters:
Return type:

list[RedistrictingPlan]

get_plan(plan_id)[source]

Get a single plan by ID.

Parameters:

plan_id (str)

Return type:

RedistrictingPlan | None

class siege_utilities.geo.plan_lifecycle.EnactedBy[source]

Bases: str, Enum

Authority that enacted the plan.

LEGISLATURE = 'legislature'
COMMISSION = 'commission'
COURT = 'court'
SPECIAL_MASTER = 'special_master'
__new__(value)
class siege_utilities.geo.plan_lifecycle.PlanLifecycleEvent[source]

Bases: object

A lifecycle transition for a redistricting plan.

plan_id

Identifier of the plan this event belongs to.

Type:

str

status

New lifecycle status after this event.

Type:

siege_utilities.geo.plan_lifecycle.PlanLifecycleStatus

effective_date

When this status took effect.

Type:

datetime.date | None

source

What triggered the transition (legislation, court order, etc.).

Type:

str

notes

Human-readable description.

Type:

str

court_case

Case citation if court-ordered.

Type:

str

plan_id: str = ''
status: PlanLifecycleStatus = 'proposed'
effective_date: date | None = None
source: str = ''
notes: str = ''
court_case: str = ''
__init__(plan_id='', status=PlanLifecycleStatus.PROPOSED, effective_date=None, source='', notes='', court_case='')
Parameters:
Return type:

None

class siege_utilities.geo.plan_lifecycle.PlanLifecycleProvider[source]

Bases: ABC

Protocol for querying redistricting plan lifecycle data.

abstractmethod get_plans(state, plan_type=None)[source]

Get all plans for a state, optionally filtered by type.

Parameters:
Return type:

list[RedistrictingPlan]

abstractmethod get_plan(plan_id)[source]

Get a single plan by ID.

Parameters:

plan_id (str)

Return type:

RedistrictingPlan | None

class siege_utilities.geo.plan_lifecycle.PlanLifecycleStatus[source]

Bases: str, Enum

Status in the lifecycle of a redistricting plan.

PROPOSED = 'proposed'
ADOPTED = 'adopted'
ENACTED = 'enacted'
CHALLENGED = 'challenged'
STAYED = 'stayed'
STRUCK = 'struck'
SUPERSEDED = 'superseded'
EXPIRED = 'expired'
__new__(value)
class siege_utilities.geo.plan_lifecycle.PlanLineage[source]

Bases: object

Chain of plans linked by supersession.

plans

Plans in chronological order (oldest first).

Type:

list[siege_utilities.geo.plan_lifecycle.RedistrictingPlan]

state

State abbreviation.

Type:

str

plan_type

Type of plan.

Type:

siege_utilities.geo.plan_lifecycle.PlanType

plans: list[RedistrictingPlan]
state: str = ''
plan_type: PlanType = 'congressional'
property current_plan: RedistrictingPlan | None
plan_at(when)[source]
Parameters:

when (date)

Return type:

RedistrictingPlan | None

__init__(plans=<factory>, state='', plan_type=PlanType.CONGRESSIONAL)
Parameters:
Return type:

None

class siege_utilities.geo.plan_lifecycle.PlanType[source]

Bases: str, Enum

Type of redistricting plan.

CONGRESSIONAL = 'congressional'
STATE_SENATE = 'state_senate'
STATE_HOUSE = 'state_house'
__new__(value)
class siege_utilities.geo.plan_lifecycle.RedistrictingPlan[source]

Bases: object

A redistricting plan with lifecycle events.

plan_id

Unique identifier.

Type:

str

state

Two-letter state abbreviation.

Type:

str

plan_type

Congressional, state senate, or state house.

Type:

siege_utilities.geo.plan_lifecycle.PlanType

plan_name

Official name or identifier.

Type:

str

enacted_by

Authority that enacted the plan.

Type:

siege_utilities.geo.plan_lifecycle.EnactedBy

enacted_date

When the plan was enacted.

Type:

datetime.date | None

supersedes_id

ID of the plan this one replaces.

Type:

str | None

court_case

Court case citation if court-ordered.

Type:

str

events

Ordered lifecycle events.

Type:

list[siege_utilities.geo.plan_lifecycle.PlanLifecycleEvent]

plan_id: str = ''
state: str = ''
plan_type: PlanType = 'congressional'
plan_name: str = ''
enacted_by: EnactedBy = 'legislature'
enacted_date: date | None = None
supersedes_id: str | None = None
court_case: str = ''
events: list[PlanLifecycleEvent]
property current_status: PlanLifecycleStatus | None
property latest_event: PlanLifecycleEvent | None
property is_terminal: bool
property is_active: bool
status_at(when)[source]
Parameters:

when (date)

Return type:

PlanLifecycleStatus | None

was_active_at(when)[source]
Parameters:

when (date)

Return type:

bool

to_dict()[source]
Return type:

dict[str, Any]

__init__(plan_id='', state='', plan_type=PlanType.CONGRESSIONAL, plan_name='', enacted_by=EnactedBy.LEGISLATURE, enacted_date=None, supersedes_id=None, court_case='', events=<factory>)
Parameters:
Return type:

None

siege_utilities.geo.plan_lifecycle.build_lineage(plans)[source]

Build a supersession lineage from a list of related plans.

Plans are ordered by enacted_date.

Parameters:

plans (list[RedistrictingPlan]) – Plans that form a lineage (same state + type).

Return type:

PlanLineage

siege_utilities.geo.plan_lifecycle.resolve_plan_at(provider, state, plan_type, when)[source]

Resolve which plan was in effect at a given date.

Queries the provider for all plans of the given state and type, then finds the one that was active at the specified date.

Parameters:
  • provider (PlanLifecycleProvider) – Source for plan lifecycle data.

  • state (str) – Two-letter state abbreviation.

  • plan_type (PlanType) – Type of plan to resolve.

  • when (date) – Date to query.

Returns:

The RedistrictingPlan that was in effect, or None.

Return type:

RedistrictingPlan | None

Precinct-to-VTD reconciliation.

Reconciles RDH precinct shapefiles with Census VTDs (Voting Tabulation Districts) using spatial overlap, name matching, and official crosswalks.

class siege_utilities.geo.precinct_vtd.ConfidenceLevel[source]

Bases: str, Enum

Confidence tier for a reconciliation mapping.

HIGH = 'high'
MEDIUM = 'medium'
LOW = 'low'
__new__(value)
class siege_utilities.geo.precinct_vtd.DictNameMatchProvider[source]

Bases: NameMatchProvider

In-memory name match provider for testing.

__init__()[source]
add_precinct(precinct_id, name)[source]
Parameters:
  • precinct_id (str)

  • name (str)

add_vtd(vtd_geoid, name)[source]
Parameters:
get_precinct_names(precinct_ids)[source]

Get display names for precincts.

Parameters:

precinct_ids (list[str])

Return type:

dict[str, str]

get_vtd_names()[source]

Get display names for VTDs (geoid → name).

Return type:

dict[str, str]

class siege_utilities.geo.precinct_vtd.DictSpatialOverlapProvider[source]

Bases: SpatialOverlapProvider

In-memory spatial overlap provider for testing.

__init__()[source]
add(overlap)[source]
Parameters:

overlap (SpatialOverlap)

compute_overlaps(precinct_ids)[source]

Compute spatial overlaps for the given precincts.

Parameters:

precinct_ids (list[str])

Return type:

list[SpatialOverlap]

class siege_utilities.geo.precinct_vtd.NameMatchProvider[source]

Bases: ABC

Protocol for matching precinct names to VTD names.

abstractmethod get_precinct_names(precinct_ids)[source]

Get display names for precincts.

Parameters:

precinct_ids (list[str])

Return type:

dict[str, str]

abstractmethod get_vtd_names()[source]

Get display names for VTDs (geoid → name).

Return type:

dict[str, str]

class siege_utilities.geo.precinct_vtd.OfficialCrosswalkEntry[source]

Bases: object

Entry from an official precinct-to-VTD crosswalk.

precinct_id

Precinct identifier as published.

Type:

str

vtd_geoid

VTD GEOID as published.

Type:

str

state

State abbreviation.

Type:

str

county

County name or FIPS.

Type:

str

source

Source of the crosswalk (e.g., “NC SBE 2022”).

Type:

str

precinct_id: str = ''
vtd_geoid: str = ''
state: str = ''
county: str = ''
source: str = ''
__init__(precinct_id='', vtd_geoid='', state='', county='', source='')
Parameters:
Return type:

None

class siege_utilities.geo.precinct_vtd.PrecinctVTDMapping[source]

Bases: object

A single precinct-to-VTD mapping with confidence metadata.

precinct_id

Identifier from the precinct shapefile.

Type:

str

vtd_geoid

Census VTD GEOID.

Type:

str

overlap_pct

Fraction of precinct area covered by this VTD (0-1).

Type:

float

confidence

Confidence score (0-1).

Type:

float

confidence_level

Categorical confidence tier.

Type:

siege_utilities.geo.precinct_vtd.ConfidenceLevel

method

How this mapping was determined.

Type:

siege_utilities.geo.precinct_vtd.ReconciliationMethod

name_similarity

Similarity score from name matching (0-1), if applicable.

Type:

float | None

notes

Human-readable explanation.

Type:

str

precinct_id: str = ''
vtd_geoid: str = ''
overlap_pct: float = 0.0
confidence: float = 0.0
confidence_level: ConfidenceLevel = 'low'
method: ReconciliationMethod = 'spatial'
name_similarity: float | None = None
notes: str = ''
to_dict()[source]
Return type:

dict[str, Any]

__init__(precinct_id='', vtd_geoid='', overlap_pct=0.0, confidence=0.0, confidence_level=ConfidenceLevel.LOW, method=ReconciliationMethod.SPATIAL, name_similarity=None, notes='')
Parameters:
Return type:

None

class siege_utilities.geo.precinct_vtd.PrecinctVTDReconciler[source]

Bases: object

Reconciles precinct boundaries with Census VTDs.

Uses spatial overlap, name matching, and official crosswalks (when available) to produce precinct-to-VTD mappings with confidence scores.

Parameters:
  • spatial_provider – Source for spatial overlap computation.

  • name_provider – Source for precinct/VTD name matching.

  • official_crosswalk – Pre-loaded official crosswalk entries.

__init__(spatial_provider=None, name_provider=None, official_crosswalk=None)[source]
Parameters:
reconcile(precinct_ids)[source]

Reconcile the given precincts against VTDs.

Parameters:

precinct_ids (list[str]) – List of precinct identifiers to reconcile.

Returns:

ReconciliationResult with mappings and diagnostics.

Return type:

ReconciliationResult

class siege_utilities.geo.precinct_vtd.ReconciliationMethod[source]

Bases: str, Enum

How a precinct-VTD mapping was determined.

SPATIAL = 'spatial'
NAME_MATCH = 'name_match'
OFFICIAL_CROSSWALK = 'official_crosswalk'
COMBINED = 'combined'
__new__(value)
class siege_utilities.geo.precinct_vtd.ReconciliationResult[source]

Bases: object

Complete result of precinct-VTD reconciliation.

mappings

All precinct-VTD mappings.

Type:

list[siege_utilities.geo.precinct_vtd.PrecinctVTDMapping]

total_precincts

Number of precincts processed.

Type:

int

matched_precincts

Number of precincts with at least one mapping.

Type:

int

unmatched_precincts

Precinct IDs with no mapping.

Type:

list[str]

method_counts

Count of mappings by method.

Type:

dict[str, int]

errors

Any errors encountered.

Type:

list[str]

mappings: list[PrecinctVTDMapping]
total_precincts: int = 0
matched_precincts: int = 0
unmatched_precincts: list[str]
method_counts: dict[str, int]
errors: list[str]
property match_rate: float
property high_confidence_count: int
for_precinct(precinct_id)[source]
Parameters:

precinct_id (str)

Return type:

list[PrecinctVTDMapping]

to_dict()[source]
Return type:

dict[str, Any]

__init__(mappings=<factory>, total_precincts=0, matched_precincts=0, unmatched_precincts=<factory>, method_counts=<factory>, errors=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.precinct_vtd.SpatialOverlap[source]

Bases: object

Spatial overlap between a precinct and a VTD.

precinct_id

Precinct identifier.

Type:

str

vtd_geoid

VTD GEOID.

Type:

str

overlap_area

Area of intersection (in source CRS units).

Type:

float

precinct_area

Total area of the precinct.

Type:

float

vtd_area

Total area of the VTD.

Type:

float

overlap_pct

Fraction of precinct covered by VTD.

Type:

float

precinct_id: str = ''
vtd_geoid: str = ''
overlap_area: float = 0.0
precinct_area: float = 0.0
vtd_area: float = 0.0
overlap_pct: float = 0.0
__init__(precinct_id='', vtd_geoid='', overlap_area=0.0, precinct_area=0.0, vtd_area=0.0, overlap_pct=0.0)
Parameters:
Return type:

None

class siege_utilities.geo.precinct_vtd.SpatialOverlapProvider[source]

Bases: ABC

Protocol for computing spatial overlaps between precincts and VTDs.

abstractmethod compute_overlaps(precinct_ids)[source]

Compute spatial overlaps for the given precincts.

Parameters:

precinct_ids (list[str])

Return type:

list[SpatialOverlap]

siege_utilities.geo.precinct_vtd.reconcile_names(precinct_names, vtd_names)[source]

Create mappings from fuzzy name matching.

Parameters:
Return type:

list[PrecinctVTDMapping]

siege_utilities.geo.precinct_vtd.reconcile_official(crosswalk)[source]

Create mappings from an official crosswalk.

Parameters:

crosswalk (list[OfficialCrosswalkEntry])

Return type:

list[PrecinctVTDMapping]

siege_utilities.geo.precinct_vtd.reconcile_spatial(overlaps)[source]

Create mappings from spatial overlaps.

Parameters:

overlaps (list[SpatialOverlap])

Return type:

list[PrecinctVTDMapping]

Codification

Address-based area codification.

Given a set of addresses, geocodes them, assigns to Census blocks, and builds a structured area portrait with demographics, urbanicity, and recency analysis.

Usage:

from siege_utilities.geo.codification import codify_area
result = codify_area(["123 Main St, Austin, TX", ...])
print(result.block_count)
print(result.summarize())
class siege_utilities.geo.codification.BlockProfile[source]

Bases: object

Profile of a single Census block within the codified area.

block_geoid

15-digit Census block GEOID.

Type:

str

tract_geoid

11-digit Census tract GEOID.

Type:

str

county_geoid

5-digit county GEOID.

Type:

str

state_geoid

2-digit state GEOID.

Type:

str

address_count

Number of geocoded addresses in this block.

Type:

int

demographics

Block-level demographic data (populated by T3).

Type:

dict[str, Any]

urbanicity

NCES locale classification (populated by T4).

Type:

dict[str, Any]

recency

Address vintage data (populated by T5).

Type:

dict[str, Any]

block_geoid: str = ''
tract_geoid: str = ''
county_geoid: str = ''
state_geoid: str = ''
address_count: int = 0
demographics: dict[str, Any]
urbanicity: dict[str, Any]
recency: dict[str, Any]
__init__(block_geoid='', tract_geoid='', county_geoid='', state_geoid='', address_count=0, demographics=<factory>, urbanicity=<factory>, recency=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.codification.CodificationResult[source]

Bases: object

Structured area portrait from address codification.

total_addresses

Total addresses submitted.

Type:

int

matched_addresses

Successfully geocoded addresses.

Type:

int

match_rate

Fraction geocoded.

Type:

float

blocks

Per-block profiles.

Type:

list[siege_utilities.geo.codification.BlockProfile]

geocoding_backend

Which geocoder was used.

Type:

str

census_year

Census vintage for geography.

Type:

int | None

errors

Any errors encountered.

Type:

list[str]

total_addresses: int = 0
matched_addresses: int = 0
match_rate: float = 0.0
blocks: list[BlockProfile]
geocoding_backend: str = ''
census_year: int | None = None
errors: list[str]
property block_count: int
property tract_count: int
property county_count: int
property state_count: int
top_blocks(n=10)[source]
Parameters:

n (int)

Return type:

list[BlockProfile]

summarize()[source]
Return type:

dict[str, Any]

__init__(total_addresses=0, matched_addresses=0, match_rate=0.0, blocks=<factory>, geocoding_backend='', census_year=None, errors=<factory>)
Parameters:
Return type:

None

siege_utilities.geo.codification.codify_area(addresses, geocoder=None, census_year=None, **kwargs)[source]

Codify an area from a set of addresses.

Geocodes the addresses, assigns them to Census blocks, and builds a structured area portrait.

Parameters:
  • addresses (list[str] | list[dict]) – Addresses to codify (strings or dicts).

  • geocoder – BatchGeocoder instance. If None, uses CensusBatchGeocoder.

  • census_year (int | None) – Census vintage for geography.

  • **kwargs – Passed through to the geocoder.

Returns:

CodificationResult with block-level profiles.

Return type:

CodificationResult

Block assignment for codification.

Groups geocoded addresses by Census block GEOID and computes geographic spread metrics (block count, spatial extent, concentration).

class siege_utilities.geo.codification_blocks.BlockAssignment[source]

Bases: object

Geocoded address assigned to a Census block.

block_geoid

15-digit block GEOID.

Type:

str

tract_geoid

11-digit tract GEOID.

Type:

str

county_geoid

5-digit county GEOID.

Type:

str

state_geoid

2-digit state GEOID.

Type:

str

lat

Latitude.

Type:

float | None

lon

Longitude.

Type:

float | None

input_id

Caller’s address identifier.

Type:

str

block_geoid: str = ''
tract_geoid: str = ''
county_geoid: str = ''
state_geoid: str = ''
lat: float | None = None
lon: float | None = None
input_id: str = ''
__init__(block_geoid='', tract_geoid='', county_geoid='', state_geoid='', lat=None, lon=None, input_id='')
Parameters:
  • block_geoid (str)

  • tract_geoid (str)

  • county_geoid (str)

  • state_geoid (str)

  • lat (float | None)

  • lon (float | None)

  • input_id (str)

Return type:

None

class siege_utilities.geo.codification_blocks.BlockGroup[source]

Bases: object

Addresses grouped by Census block.

block_geoid

Block identifier.

Type:

str

tract_geoid

Parent tract.

Type:

str

county_geoid

Parent county.

Type:

str

state_geoid

Parent state.

Type:

str

address_count

Number of addresses in this block.

Type:

int

centroid_lat

Mean latitude of addresses.

Type:

float | None

centroid_lon

Mean longitude of addresses.

Type:

float | None

block_geoid: str = ''
tract_geoid: str = ''
county_geoid: str = ''
state_geoid: str = ''
address_count: int = 0
centroid_lat: float | None = None
centroid_lon: float | None = None
__init__(block_geoid='', tract_geoid='', county_geoid='', state_geoid='', address_count=0, centroid_lat=None, centroid_lon=None)
Parameters:
  • block_geoid (str)

  • tract_geoid (str)

  • county_geoid (str)

  • state_geoid (str)

  • address_count (int)

  • centroid_lat (float | None)

  • centroid_lon (float | None)

Return type:

None

class siege_utilities.geo.codification_blocks.SpreadMetrics[source]

Bases: object

Geographic spread metrics for a set of block assignments.

block_count

Distinct blocks with addresses.

Type:

int

tract_count

Distinct tracts.

Type:

int

county_count

Distinct counties.

Type:

int

state_count

Distinct states.

Type:

int

concentration

Herfindahl index of block address distribution (0=evenly spread, 1=all in one block).

Type:

float

max_block_pct

Fraction of addresses in the most populated block.

Type:

float

bbox_lat_range

Latitude range (max - min) of all addresses.

Type:

float

bbox_lon_range

Longitude range (max - min) of all addresses.

Type:

float

block_count: int = 0
tract_count: int = 0
county_count: int = 0
state_count: int = 0
concentration: float = 0.0
max_block_pct: float = 0.0
bbox_lat_range: float = 0.0
bbox_lon_range: float = 0.0
__init__(block_count=0, tract_count=0, county_count=0, state_count=0, concentration=0.0, max_block_pct=0.0, bbox_lat_range=0.0, bbox_lon_range=0.0)
Parameters:
  • block_count (int)

  • tract_count (int)

  • county_count (int)

  • state_count (int)

  • concentration (float)

  • max_block_pct (float)

  • bbox_lat_range (float)

  • bbox_lon_range (float)

Return type:

None

siege_utilities.geo.codification_blocks.assign_blocks(geocoded_results)[source]

Convert geocoding results to block assignments.

Parameters:

geocoded_results – List of GeocodingResult from a BatchGeocoder.

Returns:

List of BlockAssignment for matched results with coordinates.

Return type:

list[BlockAssignment]

siege_utilities.geo.codification_blocks.group_by_block(assignments)[source]

Group block assignments by block GEOID.

Returns groups sorted by address count (descending).

Parameters:

assignments (list[BlockAssignment])

Return type:

list[BlockGroup]

siege_utilities.geo.codification_blocks.compute_spread(assignments, groups=None)[source]

Compute geographic spread metrics for block assignments.

Parameters:
  • assignments (list[BlockAssignment]) – All block assignments.

  • groups (list[BlockGroup] | None) – Pre-computed block groups (optional, computed if not provided).

Return type:

SpreadMetrics

Demographic enrichment for codification.

Fetches block-level demographics for matched blocks and produces an address-weighted demographic signature: race/ethnicity distribution, housing occupancy, voting-age composition.

class siege_utilities.geo.codification_demographics.BlockDemographics[source]

Bases: object

PL 94-171 demographics for a single Census block.

block_geoid

Block GEOID.

Type:

str

total_population

Total population.

Type:

int

voting_age_population

18+ population.

Type:

int

white

White alone.

Type:

int

black

Black alone.

Type:

int

hispanic

Hispanic or Latino.

Type:

int

asian

Asian alone.

Type:

int

other_race

All other races.

Type:

int

housing_total

Total housing units.

Type:

int

housing_occupied

Occupied housing units.

Type:

int

housing_vacant

Vacant housing units.

Type:

int

block_geoid: str = ''
total_population: int = 0
voting_age_population: int = 0
white: int = 0
black: int = 0
hispanic: int = 0
asian: int = 0
other_race: int = 0
housing_total: int = 0
housing_occupied: int = 0
housing_vacant: int = 0
__init__(block_geoid='', total_population=0, voting_age_population=0, white=0, black=0, hispanic=0, asian=0, other_race=0, housing_total=0, housing_occupied=0, housing_vacant=0)
Parameters:
  • block_geoid (str)

  • total_population (int)

  • voting_age_population (int)

  • white (int)

  • black (int)

  • hispanic (int)

  • asian (int)

  • other_race (int)

  • housing_total (int)

  • housing_occupied (int)

  • housing_vacant (int)

Return type:

None

class siege_utilities.geo.codification_demographics.DemographicSignature[source]

Bases: object

Address-weighted demographic signature for a codified area.

All percentages are weighted by address count per block.

total_block_population

Sum of block populations (unweighted).

Type:

int

weighted_blocks

Number of blocks with both addresses and demographics.

Type:

int

pct_white

Address-weighted white percentage.

Type:

float

pct_black

Address-weighted black percentage.

Type:

float

pct_hispanic

Address-weighted Hispanic percentage.

Type:

float

pct_asian

Address-weighted Asian percentage.

Type:

float

pct_other

Address-weighted other race percentage.

Type:

float

pct_voting_age

Address-weighted voting-age percentage.

Type:

float

pct_housing_occupied

Address-weighted housing occupancy rate.

Type:

float

pct_housing_vacant

Address-weighted vacancy rate.

Type:

float

total_block_population: int = 0
weighted_blocks: int = 0
pct_white: float = 0.0
pct_black: float = 0.0
pct_hispanic: float = 0.0
pct_asian: float = 0.0
pct_other: float = 0.0
pct_voting_age: float = 0.0
pct_housing_occupied: float = 0.0
pct_housing_vacant: float = 0.0
__init__(total_block_population=0, weighted_blocks=0, pct_white=0.0, pct_black=0.0, pct_hispanic=0.0, pct_asian=0.0, pct_other=0.0, pct_voting_age=0.0, pct_housing_occupied=0.0, pct_housing_vacant=0.0)
Parameters:
Return type:

None

class siege_utilities.geo.codification_demographics.BlockDemographicsProvider[source]

Bases: ABC

Protocol for fetching block-level demographics.

abstractmethod get_demographics(block_geoids, census_year=None)[source]

Fetch demographics for given block GEOIDs.

Returns dict of block_geoid → BlockDemographics.

Parameters:
  • block_geoids (list[str])

  • census_year (int | None)

Return type:

dict[str, BlockDemographics]

class siege_utilities.geo.codification_demographics.DictBlockDemographicsProvider[source]

Bases: BlockDemographicsProvider

In-memory demographics provider for testing.

__init__()[source]
add(demo)[source]
Parameters:

demo (BlockDemographics)

get_demographics(block_geoids, census_year=None)[source]

Fetch demographics for given block GEOIDs.

Returns dict of block_geoid → BlockDemographics.

Parameters:
  • block_geoids (list[str])

  • census_year (int | None)

Return type:

dict[str, BlockDemographics]

siege_utilities.geo.codification_demographics.compute_demographic_signature(block_address_counts, demographics)[source]

Compute address-weighted demographic signature.

Parameters:
  • block_address_counts (dict[str, int]) – Dict of block_geoid → address count.

  • demographics (dict[str, BlockDemographics]) – Dict of block_geoid → BlockDemographics.

Returns:

DemographicSignature with address-weighted percentages.

Return type:

DemographicSignature

Recency analysis for codification.

Compares address presence across TIGER vintages to produce a construction timeline: when did these addresses first appear?

class siege_utilities.geo.codification_recency.AddressVintage[source]

Bases: object

Earliest known TIGER vintage for an address.

input_id

Address identifier.

Type:

str

first_vintage

Year the address first appeared in TIGER.

Type:

int | None

present_in

List of vintages where the address was found.

Type:

list[int]

input_id: str = ''
first_vintage: int | None = None
present_in: list[int]
property is_new_construction: bool
__init__(input_id='', first_vintage=None, present_in=<factory>)
Parameters:
  • input_id (str)

  • first_vintage (int | None)

  • present_in (list[int])

Return type:

None

class siege_utilities.geo.codification_recency.RecencyAnalysis[source]

Bases: object

Address vintage distribution for a codified area.

total_analyzed

Total addresses with vintage data.

Type:

int

vintage_distribution

Dict of vintage_year → count of addresses.

Type:

dict[int, int]

vintage_pct

Dict of vintage_year → fraction of addresses.

Type:

dict[int, float]

new_construction_count

Addresses first appearing in most recent vintage.

Type:

int

new_construction_pct

Fraction of addresses that are new construction.

Type:

float

median_vintage

Median first-appearance year.

Type:

int | None

total_analyzed: int = 0
vintage_distribution: dict[int, int]
vintage_pct: dict[int, float]
new_construction_count: int = 0
new_construction_pct: float = 0.0
median_vintage: int | None = None
__init__(total_analyzed=0, vintage_distribution=<factory>, vintage_pct=<factory>, new_construction_count=0, new_construction_pct=0.0, median_vintage=None)
Parameters:
Return type:

None

class siege_utilities.geo.codification_recency.AddressVintageProvider[source]

Bases: ABC

Protocol for determining address vintage from TIGER data.

abstractmethod get_vintages(input_ids)[source]

Determine first TIGER vintage for each address.

Parameters:

input_ids (list[str])

Return type:

list[AddressVintage]

class siege_utilities.geo.codification_recency.DictAddressVintageProvider[source]

Bases: AddressVintageProvider

In-memory vintage provider for testing.

__init__()[source]
add(vintage)[source]
Parameters:

vintage (AddressVintage)

get_vintages(input_ids)[source]

Determine first TIGER vintage for each address.

Parameters:

input_ids (list[str])

Return type:

list[AddressVintage]

siege_utilities.geo.codification_recency.compute_recency(vintages)[source]

Compute recency analysis from address vintages.

Parameters:

vintages (list[AddressVintage]) – List of AddressVintage with first_vintage set.

Return type:

RecencyAnalysis

Summary portrait for codification.

Assembles headline metrics from block assignment, demographics, urbanicity, and recency into a structured area portrait.

class siege_utilities.geo.codification_summary.AreaPortrait[source]

Bases: object

Structured summary portrait of a codified area.

Combines headline metrics from all codification enrichment layers.

geocoding_summary: dict[str, Any]
spread: SpreadMetrics | None = None
demographics: DemographicSignature | None = None
urbanicity: UrbanicityDistribution | None = None
recency: RecencyAnalysis | None = None
top_blocks: list[BlockGroup]
headline_metrics()[source]
Return type:

dict[str, Any]

to_dict()[source]
Return type:

dict[str, Any]

__init__(geocoding_summary=<factory>, spread=None, demographics=None, urbanicity=None, recency=None, top_blocks=<factory>)
Parameters:
Return type:

None

siege_utilities.geo.codification_summary.build_portrait(codification, spread=None, demographics=None, urbanicity=None, recency=None, top_n=10)[source]

Build a structured area portrait from codification results.

Parameters:
Return type:

AreaPortrait

Urbanicity enrichment for codification.

Classifies each matched block’s urbanicity using the NCES locale system and produces an address-weighted distribution.

class siege_utilities.geo.codification_urbanicity.BlockLocale[source]

Bases: object

NCES locale classification for a Census block.

block_geoid

Block GEOID.

Type:

str

locale_code

NCES 2-digit code (e.g., “11”).

Type:

str

locale_label

Human-readable label (e.g., “City-Large”).

Type:

str

category

Top-level (city, suburb, town, rural).

Type:

str

block_geoid: str = ''
locale_code: str = ''
locale_label: str = ''
category: str = ''
__init__(block_geoid='', locale_code='', locale_label='', category='')
Parameters:
  • block_geoid (str)

  • locale_code (str)

  • locale_label (str)

  • category (str)

Return type:

None

class siege_utilities.geo.codification_urbanicity.UrbanicityDistribution[source]

Bases: object

Address-weighted urbanicity distribution for a codified area.

total_classified

Number of addresses with urbanicity classification.

Type:

int

distribution

Dict of locale_code → fraction of addresses.

Type:

dict[str, float]

category_distribution

Dict of category → fraction of addresses.

Type:

dict[str, float]

dominant_locale

Most common locale code.

Type:

str

dominant_category

Most common category.

Type:

str

total_classified: int = 0
distribution: dict[str, float]
category_distribution: dict[str, float]
dominant_locale: str = ''
dominant_category: str = ''
__init__(total_classified=0, distribution=<factory>, category_distribution=<factory>, dominant_locale='', dominant_category='')
Parameters:
Return type:

None

class siege_utilities.geo.codification_urbanicity.BlockLocaleProvider[source]

Bases: ABC

Protocol for fetching block urbanicity classifications.

abstractmethod classify_blocks(block_geoids, census_year=None)[source]

Classify blocks by NCES locale.

Returns dict of block_geoid → BlockLocale.

Parameters:
  • block_geoids (list[str])

  • census_year (int | None)

Return type:

dict[str, BlockLocale]

class siege_utilities.geo.codification_urbanicity.DictBlockLocaleProvider[source]

Bases: BlockLocaleProvider

In-memory locale provider for testing.

__init__()[source]
add(locale)[source]
Parameters:

locale (BlockLocale)

classify_blocks(block_geoids, census_year=None)[source]

Classify blocks by NCES locale.

Returns dict of block_geoid → BlockLocale.

Parameters:
  • block_geoids (list[str])

  • census_year (int | None)

Return type:

dict[str, BlockLocale]

siege_utilities.geo.codification_urbanicity.compute_urbanicity_distribution(block_address_counts, locales)[source]

Compute address-weighted urbanicity distribution.

Parameters:
  • block_address_counts (dict[str, int]) – Dict of block_geoid → address count.

  • locales (dict[str, BlockLocale]) – Dict of block_geoid → BlockLocale.

Return type:

UrbanicityDistribution

Schemas

Pydantic schema layer for geographic models.

Provides validation schemas that mirror the Django models but without geometry (geometry handled via GeoDataFrame/WKT). Designed for: - API serialization/validation - Data pipeline intermediate representations - Round-trip conversion: GeoDataFrame ↔ Schema ↔ ORM

Usage:

from siege_utilities.geo.schemas import StateSchema, CountySchema from siege_utilities.geo.schemas.converters import gdf_to_schemas, schemas_to_orm

class siege_utilities.geo.schemas.TemporalGeographicFeatureSchema[source]

Bases: BaseModel

Schema for TemporalGeographicFeature fields.

valid_from: date | None = None
valid_to: date | None = None
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.TemporalBoundarySchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for TemporalBoundary fields (no geometry).

area_land: int | None = None
area_water: int | None = None
internal_point_wkt: str | None = None
sync_ids()
class siege_utilities.geo.schemas.CensusTIGERSchema[source]

Bases: TemporalBoundarySchema

Schema for CensusTIGERBoundary fields.

sync_from_geoid()
class siege_utilities.geo.schemas.StateSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.CountySchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.TractSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.BlockGroupSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.BlockSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.PlaceSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.ZCTASchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.CongressionalDistrictSchema[source]

Bases: CensusTIGERSchema

congress_number: int | None = None
class siege_utilities.geo.schemas.StateLegislativeUpperSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.StateLegislativeLowerSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.VTDSchema[source]

Bases: CensusTIGERSchema

registered_voters: int | None = None
class siege_utilities.geo.schemas.PrecinctSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.GADMBoundarySchema[source]

Bases: TemporalBoundarySchema

class siege_utilities.geo.schemas.GADMCountrySchema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.GADMAdmin1Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.GADMAdmin2Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.GADMAdmin3Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.GADMAdmin4Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.GADMAdmin5Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.SchoolDistrictBaseSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.SchoolDistrictElementarySchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.SchoolDistrictSecondarySchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.SchoolDistrictUnifiedSchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.NCESLocaleBoundarySchema[source]

Bases: TemporalBoundarySchema

Schema for NCESLocaleBoundary.

class siege_utilities.geo.schemas.SchoolLocationSchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for SchoolLocation (point feature, no geometry in schema).

class siege_utilities.geo.schemas.NLRBRegionSchema[source]

Bases: TemporalBoundarySchema

region_number: int
class siege_utilities.geo.schemas.FederalJudicialDistrictSchema[source]

Bases: TemporalBoundarySchema

circuit_number: int | None = None
class siege_utilities.geo.schemas.BoundaryIntersectionSchema[source]

Bases: BaseModel

intersection_area: int | None = None
pct_of_source: Decimal | None = None
pct_of_target: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.CountyCDIntersectionSchema[source]

Bases: BaseModel

county_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_county: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.VTDCDIntersectionSchema[source]

Bases: BaseModel

vtd_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_vtd: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.TractCDIntersectionSchema[source]

Bases: BaseModel

tract_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_tract: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.TemporalCrosswalkSchema[source]

Bases: BaseModel

source_population: int | None = None
target_population: int | None = None
allocated_population: int | None = None
area_sq_meters: int | None = None
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.DemographicVariableSchema[source]

Bases: BaseModel

Reference schema for a Census variable.

concept: str = ''
predicate_type: str = ''
group: str = ''
universe: str = ''
model_config = {'from_attributes': True}
property is_estimate: bool

True if this is an estimate variable (ends in E).

property is_moe: bool

True if this is a margin of error variable (ends in M).

property base_code: str

Return the code without the E/M suffix.

class siege_utilities.geo.schemas.DemographicSnapshotSchema[source]

Bases: BaseModel

One demographic snapshot for a boundary.

Uses boundary_type + boundary_id instead of Django’s generic FK.

vintage: int | None = None
total_population: int | None = None
median_household_income: int | None = None
median_age: float | None = None
source_url: str = ''
model_config = {'from_attributes': True}
auto_populate_summaries()

Auto-populate summary fields from values, mirroring Django save().

get_value(variable_code, default=None)[source]

Get a specific variable value.

Parameters:

variable_code (str)

get_moe(variable_code, default=None)[source]

Get margin of error for a specific variable.

Parameters:

variable_code (str)

class siege_utilities.geo.schemas.DemographicTimeSeriesSchema[source]

Bases: BaseModel

Pre-computed time series for a single variable on a single boundary.

start_year: int
end_year: int
years: list[int]
values: list[float | int]
mean_value: float | None = None
std_dev: float | None = None
cagr: float | None = None
trend_direction: str = ''
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.RedistrictingPlanSchema[source]

Bases: BaseModel

Schema for a redistricting plan with temporal resolution.

Supports court-ordered mid-cycle redistricting: multiple plans can exist for the same state/chamber/cycle with different effective date ranges. Use for_date(state_fips, chamber, date) on the Django model to find which plan was active on a given date.

source_url: str = ''
num_districts: int
enacted_date: date | None = None
effective_from: date | None = None
effective_to: date | None = None
superseded_by_id: int | None = None
court_case: str = ''
model_config = {'from_attributes': True}
validate_effective_range()

Effective range must be valid if both dates are set.

class siege_utilities.geo.schemas.PlanDistrictSchema[source]

Bases: CensusTIGERSchema

Schema for a single district within a redistricting plan.

plan_id: int | str | None = None
total_population: int | None = None
vap: int | None = None
cvap: int | None = None
deviation_pct: float | None = None
class siege_utilities.geo.schemas.DistrictDemographicsSchema[source]

Bases: BaseModel

Schema for demographic profile of a plan district.

district_id: int | str
pop_white: int | None = None
pop_black: int | None = None
pop_hispanic: int | None = None
pop_asian: int | None = None
pop_native: int | None = None
pop_other: int | None = None
pop_two_or_more: int | None = None
median_household_income: int | None = None
model_config = {'from_attributes': True}
auto_populate_race_from_values()

Auto-populate race fields from Census variable codes if present.

class siege_utilities.geo.schemas.PrecinctElectionResultSchema[source]

Bases: BaseModel

Schema for precinct-level election results.

votes: int
total_votes: int | None = None
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.CongressionalTermSchema[source]

Bases: BaseModel

Schema for CongressionalTerm.

start_date: date
end_date: date
election_year: int
is_presidential: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.SeatSchema[source]

Bases: BaseModel

Schema for Seat.

is_active: bool = True
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.StateElectionCalendarSchema[source]

Bases: BaseModel

Schema for StateElectionCalendar.

primary_date: date | None = None
primary_runoff_date: date | None = None
general_date: date | None = None
general_runoff_date: date | None = None
registration_deadline: date | None = None
early_voting_start: date | None = None
early_voting_end: date | None = None
mail_ballot_request_deadline: date | None = None
mail_ballot_return_deadline: date | None = None
certification_deadline: date | None = None
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.RaceSchema[source]

Bases: BaseModel

Schema for Race.

seat_id: int | str
is_special: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.RaceEventSchema[source]

Bases: BaseModel

Schema for RaceEvent.

race_id: int | str
event_date: date
event_start: datetime | None = None
event_end: datetime | None = None
external_event_id: int | str | None = None
notes: str = ''
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.SpatioTemporalEventSchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for SpatioTemporalEvent.

event_start: datetime
event_end: datetime | None = None
description: str = ''
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.ReturnSnapshotSchema[source]

Bases: BaseModel

Schema for ReturnSnapshot.

race_id: int | str
timestamp: datetime
precincts_reporting: int | None = None
total_precincts: int | None = None
total_ballots_counted: int | None = None
ballots_outstanding: int | None = None
is_final: bool = False
model_config = {'from_attributes': True}
siege_utilities.geo.schemas.gdf_to_schemas(gdf, schema_class, column_map=None)[source]

Convert a GeoDataFrame to a list of Pydantic schema instances.

Geometry is dropped (schemas are non-spatial). Column names are mapped via column_map or auto-matched to schema field names.

Parameters:
  • gdf (gpd.GeoDataFrame) – Input GeoDataFrame

  • schema_class (type[T]) – Pydantic model class to instantiate

  • column_map (dict[str, str] | None) – Optional {gdf_column: schema_field} overrides

Returns:

List of validated schema instances

Return type:

list[T]

siege_utilities.geo.schemas.schemas_to_gdf(schemas, geometry_wkts=None, geometry_column='geometry', srid=4326, *, crs=None)[source]

Convert a list of Pydantic schema instances to a GeoDataFrame.

Complement to gdf_to_schemas(). No Django required.

Parameters:
  • schemas (list['BaseModel']) – List of Pydantic schema instances

  • geometry_wkts (list[str] | None) – Optional list of WKT geometry strings (parallel to schemas)

  • geometry_column (str) – Name for the geometry column

  • srid (int) – SRID for the CRS (default 4326). Deprecated — use crs instead.

  • crs (str | None) – Output CRS (default "EPSG:4326"). Overrides srid if provided.

Returns:

GeoDataFrame with schema data and optional geometry in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.schemas.schemas_to_orm(schemas, model_class, geometry_wkts=None, srid=4326)[source]

Convert a list of Pydantic schemas to Django ORM model instances.

Does NOT call save() — returns unsaved instances for bulk_create.

Parameters:
  • schemas (list['BaseModel']) – List of Pydantic schema instances

  • model_class (type) – Django model class to instantiate

  • geometry_wkts (list[str] | None) – Optional list of WKT geometry strings (parallel to schemas)

  • srid (int) – SRID for geometry (default 4326)

Returns:

List of unsaved Django model instances

Return type:

list

siege_utilities.geo.schemas.orm_to_gdf(queryset, geometry_field='geometry', srid=4326, *, crs=None)[source]

Convert a Django QuerySet to a GeoDataFrame.

Parameters:
  • queryset – Django QuerySet (must include geometry field)

  • geometry_field (str) – Name of the geometry field

  • srid (int) – SRID for the output CRS (default 4326). Deprecated — use crs.

  • crs (str | None) – Output CRS (default "EPSG:4326"). Overrides srid if provided.

Returns:

GeoDataFrame with geometry and all non-geometry fields in crs.

Return type:

gpd.GeoDataFrame

Base Pydantic schemas mirroring the abstract model hierarchy.

Geometry is NOT included — it’s handled via GeoDataFrame/WKT in converters.

class siege_utilities.geo.schemas.base.CensusTIGERSchema[source]

Bases: TemporalBoundarySchema

Schema for CensusTIGERBoundary fields.

sync_from_geoid()
class siege_utilities.geo.schemas.base.TemporalBoundarySchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for TemporalBoundary fields (no geometry).

area_land: int | None = None
area_water: int | None = None
internal_point_wkt: str | None = None
sync_ids()
class siege_utilities.geo.schemas.base.TemporalGeographicFeatureSchema[source]

Bases: BaseModel

Schema for TemporalGeographicFeature fields.

valid_from: date | None = None
valid_to: date | None = None
model_config = {'from_attributes': True}

Pydantic schemas for the 8 concrete Census TIGER boundary models.

class siege_utilities.geo.schemas.boundaries.BlockGroupSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.BlockSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.CongressionalDistrictSchema[source]

Bases: CensusTIGERSchema

congress_number: int | None = None
class siege_utilities.geo.schemas.boundaries.CountySchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.PlaceSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.StateSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.TractSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.boundaries.ZCTASchema[source]

Bases: CensusTIGERSchema

Conversion utilities between GeoDataFrame, Pydantic schemas, and Django ORM.

Three conversion directions:

GeoDataFrame → Schema: gdf_to_schemas(gdf, SchemaClass) Schema → ORM: schemas_to_orm(schemas, ModelClass) ORM → GeoDataFrame: orm_to_gdf(queryset)

For Django models, from_schema()/to_schema() classmethods are the idiomatic way to do single-object conversions without importing converters directly.

siege_utilities.geo.schemas.converters.gdf_to_schemas(gdf, schema_class, column_map=None)[source]

Convert a GeoDataFrame to a list of Pydantic schema instances.

Geometry is dropped (schemas are non-spatial). Column names are mapped via column_map or auto-matched to schema field names.

Parameters:
  • gdf (gpd.GeoDataFrame) – Input GeoDataFrame

  • schema_class (type[T]) – Pydantic model class to instantiate

  • column_map (dict[str, str] | None) – Optional {gdf_column: schema_field} overrides

Returns:

List of validated schema instances

Return type:

list[T]

siege_utilities.geo.schemas.converters.orm_to_gdf(queryset, geometry_field='geometry', srid=4326, *, crs=None)[source]

Convert a Django QuerySet to a GeoDataFrame.

Parameters:
  • queryset – Django QuerySet (must include geometry field)

  • geometry_field (str) – Name of the geometry field

  • srid (int) – SRID for the output CRS (default 4326). Deprecated — use crs.

  • crs (str | None) – Output CRS (default "EPSG:4326"). Overrides srid if provided.

Returns:

GeoDataFrame with geometry and all non-geometry fields in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.schemas.converters.schemas_to_gdf(schemas, geometry_wkts=None, geometry_column='geometry', srid=4326, *, crs=None)[source]

Convert a list of Pydantic schema instances to a GeoDataFrame.

Complement to gdf_to_schemas(). No Django required.

Parameters:
  • schemas (list['BaseModel']) – List of Pydantic schema instances

  • geometry_wkts (list[str] | None) – Optional list of WKT geometry strings (parallel to schemas)

  • geometry_column (str) – Name for the geometry column

  • srid (int) – SRID for the CRS (default 4326). Deprecated — use crs instead.

  • crs (str | None) – Output CRS (default "EPSG:4326"). Overrides srid if provided.

Returns:

GeoDataFrame with schema data and optional geometry in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.schemas.converters.schemas_to_orm(schemas, model_class, geometry_wkts=None, srid=4326)[source]

Convert a list of Pydantic schemas to Django ORM model instances.

Does NOT call save() — returns unsaved instances for bulk_create.

Parameters:
  • schemas (list['BaseModel']) – List of Pydantic schema instances

  • model_class (type) – Django model class to instantiate

  • geometry_wkts (list[str] | None) – Optional list of WKT geometry strings (parallel to schemas)

  • srid (int) – SRID for geometry (default 4326)

Returns:

List of unsaved Django model instances

Return type:

list

Pydantic schema for the TemporalCrosswalk model.

class siege_utilities.geo.schemas.crosswalks.TemporalCrosswalkSchema[source]

Bases: BaseModel

source_population: int | None = None
target_population: int | None = None
allocated_population: int | None = None
area_sq_meters: int | None = None
model_config = {'from_attributes': True}

Pydantic schemas for demographic data models.

Mirrors the Django models in geo/django/models/demographics.py but with plain strings (boundary_type + boundary_id) replacing Django’s generic foreign keys. No Django dependency required.

class siege_utilities.geo.schemas.demographics.DemographicSnapshotSchema[source]

Bases: BaseModel

One demographic snapshot for a boundary.

Uses boundary_type + boundary_id instead of Django’s generic FK.

vintage: int | None = None
total_population: int | None = None
median_household_income: int | None = None
median_age: float | None = None
source_url: str = ''
model_config = {'from_attributes': True}
auto_populate_summaries()

Auto-populate summary fields from values, mirroring Django save().

get_value(variable_code, default=None)[source]

Get a specific variable value.

Parameters:

variable_code (str)

get_moe(variable_code, default=None)[source]

Get margin of error for a specific variable.

Parameters:

variable_code (str)

class siege_utilities.geo.schemas.demographics.DemographicTimeSeriesSchema[source]

Bases: BaseModel

Pre-computed time series for a single variable on a single boundary.

start_year: int
end_year: int
years: list[int]
values: list[float | int]
mean_value: float | None = None
std_dev: float | None = None
cagr: float | None = None
trend_direction: str = ''
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.demographics.DemographicVariableSchema[source]

Bases: BaseModel

Reference schema for a Census variable.

concept: str = ''
predicate_type: str = ''
group: str = ''
universe: str = ''
model_config = {'from_attributes': True}
property is_estimate: bool

True if this is an estimate variable (ends in E).

property is_moe: bool

True if this is a margin of error variable (ends in M).

property base_code: str

Return the code without the E/M suffix.

Pydantic schemas for NCES education models.

class siege_utilities.geo.schemas.education.NCESLocaleBoundarySchema[source]

Bases: TemporalBoundarySchema

Schema for NCESLocaleBoundary.

class siege_utilities.geo.schemas.education.SchoolDistrictBaseSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.education.SchoolDistrictElementarySchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.education.SchoolDistrictSecondarySchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.education.SchoolDistrictUnifiedSchema[source]

Bases: SchoolDistrictBaseSchema

class siege_utilities.geo.schemas.education.SchoolLocationSchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for SchoolLocation (point feature, no geometry in schema).

Pydantic schemas for federal boundary models.

class siege_utilities.geo.schemas.federal.FederalJudicialDistrictSchema[source]

Bases: TemporalBoundarySchema

circuit_number: int | None = None
class siege_utilities.geo.schemas.federal.NLRBRegionSchema[source]

Bases: TemporalBoundarySchema

region_number: int

Pydantic schemas for GADM models.

class siege_utilities.geo.schemas.gadm.GADMAdmin1Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMAdmin2Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMAdmin3Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMAdmin4Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMAdmin5Schema[source]

Bases: GADMBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMBoundarySchema[source]

Bases: TemporalBoundarySchema

class siege_utilities.geo.schemas.gadm.GADMCountrySchema[source]

Bases: GADMBoundarySchema

Pydantic schemas for intersection models.

class siege_utilities.geo.schemas.intersections.BoundaryIntersectionSchema[source]

Bases: BaseModel

intersection_area: int | None = None
pct_of_source: Decimal | None = None
pct_of_target: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.intersections.CountyCDIntersectionSchema[source]

Bases: BaseModel

county_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_county: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.intersections.TractCDIntersectionSchema[source]

Bases: BaseModel

tract_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_tract: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.intersections.VTDCDIntersectionSchema[source]

Bases: BaseModel

vtd_id: int
congressional_district_id: int
vintage_year: int
intersection_area: int | None = None
pct_of_vtd: Decimal | None = None
pct_of_cd: Decimal | None = None
is_dominant: bool = False
model_config = {'from_attributes': True}

Pydantic schemas for political Census models.

class siege_utilities.geo.schemas.political.PrecinctSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.political.StateLegislativeLowerSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.political.StateLegislativeUpperSchema[source]

Bases: CensusTIGERSchema

class siege_utilities.geo.schemas.political.VTDSchema[source]

Bases: CensusTIGERSchema

registered_voters: int | None = None

Pydantic schemas for redistricting models.

Mirrors the Django models in geo/django/models/redistricting.py but without Django dependency. Geometry handled via GeoDataFrame/WKT in converters.

class siege_utilities.geo.schemas.redistricting.DistrictDemographicsSchema[source]

Bases: BaseModel

Schema for demographic profile of a plan district.

district_id: int | str
pop_white: int | None = None
pop_black: int | None = None
pop_hispanic: int | None = None
pop_asian: int | None = None
pop_native: int | None = None
pop_other: int | None = None
pop_two_or_more: int | None = None
median_household_income: int | None = None
model_config = {'from_attributes': True}
auto_populate_race_from_values()

Auto-populate race fields from Census variable codes if present.

class siege_utilities.geo.schemas.redistricting.PlanDistrictSchema[source]

Bases: CensusTIGERSchema

Schema for a single district within a redistricting plan.

plan_id: int | str | None = None
total_population: int | None = None
vap: int | None = None
cvap: int | None = None
deviation_pct: float | None = None
class siege_utilities.geo.schemas.redistricting.PrecinctElectionResultSchema[source]

Bases: BaseModel

Schema for precinct-level election results.

votes: int
total_votes: int | None = None
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.redistricting.RedistrictingPlanSchema[source]

Bases: BaseModel

Schema for a redistricting plan with temporal resolution.

Supports court-ordered mid-cycle redistricting: multiple plans can exist for the same state/chamber/cycle with different effective date ranges. Use for_date(state_fips, chamber, date) on the Django model to find which plan was active on a given date.

source_url: str = ''
num_districts: int
enacted_date: date | None = None
effective_from: date | None = None
effective_to: date | None = None
superseded_by_id: int | None = None
court_case: str = ''
model_config = {'from_attributes': True}
validate_effective_range()

Effective range must be valid if both dates are set.

Pydantic schemas for temporal event models (Phase B).

Mirrors Django models in geo/django/models/temporal_events.py.

class siege_utilities.geo.schemas.temporal_events.RaceEventSchema[source]

Bases: BaseModel

Schema for RaceEvent.

race_id: int | str
event_date: date
event_start: datetime | None = None
event_end: datetime | None = None
external_event_id: int | str | None = None
notes: str = ''
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.temporal_events.RaceSchema[source]

Bases: BaseModel

Schema for Race.

seat_id: int | str
is_special: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.temporal_events.ReturnSnapshotSchema[source]

Bases: BaseModel

Schema for ReturnSnapshot.

race_id: int | str
timestamp: datetime
precincts_reporting: int | None = None
total_precincts: int | None = None
total_ballots_counted: int | None = None
ballots_outstanding: int | None = None
is_final: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.temporal_events.SpatioTemporalEventSchema[source]

Bases: TemporalGeographicFeatureSchema

Schema for SpatioTemporalEvent.

event_start: datetime
event_end: datetime | None = None
description: str = ''
model_config = {'from_attributes': True}

Pydantic schemas for temporal political models (Phase A).

Mirrors Django models in geo/django/models/temporal_political.py.

class siege_utilities.geo.schemas.temporal_political.CongressionalTermSchema[source]

Bases: BaseModel

Schema for CongressionalTerm.

start_date: date
end_date: date
election_year: int
is_presidential: bool = False
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.temporal_political.SeatSchema[source]

Bases: BaseModel

Schema for Seat.

is_active: bool = True
model_config = {'from_attributes': True}
class siege_utilities.geo.schemas.temporal_political.StateElectionCalendarSchema[source]

Bases: BaseModel

Schema for StateElectionCalendar.

primary_date: date | None = None
primary_runoff_date: date | None = None
general_date: date | None = None
general_runoff_date: date | None = None
registration_deadline: date | None = None
early_voting_start: date | None = None
early_voting_end: date | None = None
mail_ballot_request_deadline: date | None = None
mail_ballot_return_deadline: date | None = None
certification_deadline: date | None = None
model_config = {'from_attributes': True}

Temporal and Timeseries

Pure-Python temporal data management for geographic boundaries and demographics.

Provides persistence, querying, and time-series construction without Django/PostGIS. All submodules load on first attribute access via PEP 562 __getattr__.

class siege_utilities.geo.temporal.TemporalDataStore[source]

Bases: object

Pure-Python persistence for temporal geographic data.

Follows the CrosswalkClient pattern: class with module-level convenience functions for the common case.

SUPPORTED_FORMATS = ('parquet', 'gpkg')
__init__(root_dir=None, format='parquet')[source]
Parameters:
Return type:

None

save_boundaries(gdf, geography_type, vintage_year)[source]

Persist a GeoDataFrame of boundaries.

Parameters:
  • gdf (gpd.GeoDataFrame)

  • geography_type (str)

  • vintage_year (int)

Return type:

Path

load_boundaries(geography_type, vintage_year, state_fips=None)[source]

Load boundaries for a geography type and vintage year.

Parameters:
  • geography_type (str)

  • vintage_year (int)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

query_boundaries_at_date(geography_type, query_date, state_fips=None)[source]

Load the best-matching vintage for a given date.

If boundaries have valid_from/valid_to columns, those are checked. Otherwise, the nearest vintage_year <= query_date.year is used.

Parameters:
  • geography_type (str)

  • query_date (date)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

list_available_vintages(geography_type)[source]

List vintage years available on disk for a geography type.

Parameters:

geography_type (str)

Return type:

list[int]

save_demographics(snapshots, geography_type, dataset='acs5', year=None)[source]

Persist demographic snapshot data as Parquet.

Parameters:
  • snapshots (pd.DataFrame)

  • geography_type (str)

  • dataset (str)

  • year (int | None)

Return type:

Path

load_demographics(geography_type, year=None, dataset='acs5')[source]

Load demographic data. If year is None, loads all available years.

Parameters:
  • geography_type (str)

  • year (int | None)

  • dataset (str)

Return type:

pd.DataFrame

save_timeseries(series, geography_type, variable_code=None, dataset='acs5')[source]

Persist pre-computed time-series data as Parquet.

Parameters:
  • series (pd.DataFrame)

  • geography_type (str)

  • variable_code (str | None)

  • dataset (str)

Return type:

Path

load_timeseries(geography_type, variable_code, dataset='acs5')[source]

Load pre-computed time-series data.

Parameters:
  • geography_type (str)

  • variable_code (str)

  • dataset (str)

Return type:

pd.DataFrame

siege_utilities.geo.temporal.get_temporal_store(root_dir=None, format='parquet')[source]

Get or create the singleton TemporalDataStore.

Parameters:
Return type:

TemporalDataStore

siege_utilities.geo.temporal.save_boundaries(gdf, geography_type, vintage_year, **kwargs)[source]

Save boundaries via the default store.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame of boundaries to persist.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.temporal.load_boundaries(geography_type, vintage_year, state_fips=None, **kwargs)[source]

Load boundaries via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.query_boundaries_at_date(geography_type, query_date, state_fips=None, **kwargs)[source]

Query boundaries at a date via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • query_date (date) – Date for which to find matching boundaries.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.save_demographics(snapshots, geography_type, dataset='acs5', year=None, **kwargs)[source]

Save demographics via the default store.

Parameters:
  • snapshots (pd.DataFrame) – DataFrame of demographic snapshot data.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • dataset (str) – Dataset identifier (default “acs5”).

  • year (int | None) – Year of the snapshot. Inferred from DataFrame if omitted.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.temporal.load_demographics(geography_type, year=None, dataset='acs5', **kwargs)[source]

Load demographics via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • year (int | None) – Year to load. If None, loads all available years.

  • dataset (str) – Dataset identifier (default “acs5”).

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

pd.DataFrame

siege_utilities.geo.temporal.temporal_filter(gdf, query_date, valid_from_col='valid_from', valid_to_col='valid_to', vintage_year_col='vintage_year')[source]

Filter a GeoDataFrame to rows valid at query_date.

First tries valid_from/valid_to date range filtering. Falls back to vintage_year if date columns are missing or all-null.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame with temporal columns

  • query_date (date) – Date to query for

  • valid_from_col (str) – Column name for validity start date

  • valid_to_col (str) – Column name for validity end date

  • vintage_year_col (str | None) – Fallback column for nearest-year matching

Returns:

Filtered GeoDataFrame

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.spatial_query(boundaries, points, predicate='intersects', *, crs=None)[source]

Spatial join between boundaries and points using geopandas.sjoin.

Aligns CRS before joining if both GeoDataFrames have a CRS set.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons (left)

  • points (gpd.GeoDataFrame) – GeoDataFrame of query geometries (right)

  • predicate (str) – Spatial predicate (intersects, within, contains, etc.)

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

Joined GeoDataFrame with columns from both inputs, in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.point_in_boundary(boundaries, lon, lat, predicate='intersects', *, crs=None)[source]

Find which boundaries contain a single point.

Convenience wrapper that creates a Point GeoDataFrame and calls spatial_query.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons

  • lon (float) – Longitude

  • lat (float) – Latitude

  • predicate (str) – Spatial predicate

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

GeoDataFrame of matching boundaries in crs.

Return type:

gpd.GeoDataFrame

class siege_utilities.geo.temporal.TemporalTimeseriesBuilder[source]

Bases: object

Pure-Python equivalent of django/services/timeseries_service.py.

Builds DemographicTimeSeriesSchema instances from snapshot DataFrames, computing the same statistics (mean, std_dev, CAGR, trend_direction) as the Django service.

Example

builder = TemporalTimeseriesBuilder(store=store, dataset=’acs5’) results = builder.build(

variables=[‘B01001_001E’], years=[2015, 2016, 2017, 2018, 2019, 2020], geography_type=’tract’,

)

__init__(store=None, dataset='acs5')[source]
Parameters:
  • store (Optional['TemporalDataStore'])

  • dataset (str)

Return type:

None

build(variables, years, geography_type, snapshots_df=None)[source]

Build time-series schemas from snapshot data.

Parameters:
  • variables (list[str]) – Census variable codes (e.g. [‘B01001_001E’])

  • years (list[int]) – Years to include in the series

  • geography_type (str) – Geography level name

  • snapshots_df (Optional['pd.DataFrame']) – Pre-loaded snapshots DataFrame with columns: boundary_id, year, values (dict column), moe_values (dict column). If None, loads from self.store.

Returns:

List of TimeseriesBuildResult (one per variable), with created DemographicTimeSeriesSchema instances accessible via result.series.

Return type:

list[TimeseriesBuildResult]

class siege_utilities.geo.temporal.TemporalDemographicService[source]

Bases: object

Build demographic snapshots from Census API data.

Converts raw Census API DataFrames into DemographicSnapshotSchema instances and optionally persists them via TemporalDataStore.

__init__(store=None, api_key=None)[source]
Parameters:
  • store (Optional['TemporalDataStore'])

  • api_key (str | None)

Return type:

None

build_snapshots(df, geography_type, year, dataset='acs5', geoid_column='GEOID', variable_columns=None)[source]

Convert a Census API DataFrame to snapshot schema instances.

Parameters:
  • df (pd.DataFrame) – DataFrame from Census API (columns: GEOID + variable codes)

  • geography_type (str) – Geography level name

  • year (int) – Census/ACS year

  • dataset (str) – Census dataset

  • geoid_column (str) – Column containing GEOIDs

  • variable_columns (list[str] | None) – Explicit list of variable columns. If None, uses all columns except geoid_column and ‘NAME’.

Returns:

List of DemographicSnapshotSchema instances

Return type:

list[DemographicSnapshotSchema]

class siege_utilities.geo.temporal.TimeseriesBuildResult[source]

Bases: object

Result of a time-series build run.

variable_code: str
geography_type: str
records_created: int = 0
records_skipped: int = 0
errors: list[str]
property total_processed: int
property success: bool
__init__(variable_code, geography_type, records_created=0, records_skipped=0, errors=<factory>)
Parameters:
  • variable_code (str)

  • geography_type (str)

  • records_created (int)

  • records_skipped (int)

  • errors (list[str])

Return type:

None

Pure-Python temporal and spatial query helpers.

Uses geopandas.sjoin for spatial queries (no PostGIS required).

siege_utilities.geo.temporal.query.temporal_filter(gdf, query_date, valid_from_col='valid_from', valid_to_col='valid_to', vintage_year_col='vintage_year')[source]

Filter a GeoDataFrame to rows valid at query_date.

First tries valid_from/valid_to date range filtering. Falls back to vintage_year if date columns are missing or all-null.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame with temporal columns

  • query_date (date) – Date to query for

  • valid_from_col (str) – Column name for validity start date

  • valid_to_col (str) – Column name for validity end date

  • vintage_year_col (str | None) – Fallback column for nearest-year matching

Returns:

Filtered GeoDataFrame

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.query.spatial_query(boundaries, points, predicate='intersects', *, crs=None)[source]

Spatial join between boundaries and points using geopandas.sjoin.

Aligns CRS before joining if both GeoDataFrames have a CRS set.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons (left)

  • points (gpd.GeoDataFrame) – GeoDataFrame of query geometries (right)

  • predicate (str) – Spatial predicate (intersects, within, contains, etc.)

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

Joined GeoDataFrame with columns from both inputs, in crs.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.query.point_in_boundary(boundaries, lon, lat, predicate='intersects', *, crs=None)[source]

Find which boundaries contain a single point.

Convenience wrapper that creates a Point GeoDataFrame and calls spatial_query.

Parameters:
  • boundaries (gpd.GeoDataFrame) – GeoDataFrame of boundary polygons

  • lon (float) – Longitude

  • lat (float) – Latitude

  • predicate (str) – Spatial predicate

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

GeoDataFrame of matching boundaries in crs.

Return type:

gpd.GeoDataFrame

Pure-Python services for building demographic time series.

Ports the core logic from django/services/timeseries_service.py to work with DataFrames and TemporalDataStore instead of Django ORM.

class siege_utilities.geo.temporal.services.TimeseriesBuildResult[source]

Bases: object

Result of a time-series build run.

variable_code: str
geography_type: str
records_created: int = 0
records_skipped: int = 0
errors: list[str]
property total_processed: int
property success: bool
__init__(variable_code, geography_type, records_created=0, records_skipped=0, errors=<factory>)
Parameters:
  • variable_code (str)

  • geography_type (str)

  • records_created (int)

  • records_skipped (int)

  • errors (list[str])

Return type:

None

class siege_utilities.geo.temporal.services.TemporalTimeseriesBuilder[source]

Bases: object

Pure-Python equivalent of django/services/timeseries_service.py.

Builds DemographicTimeSeriesSchema instances from snapshot DataFrames, computing the same statistics (mean, std_dev, CAGR, trend_direction) as the Django service.

Example

builder = TemporalTimeseriesBuilder(store=store, dataset=’acs5’) results = builder.build(

variables=[‘B01001_001E’], years=[2015, 2016, 2017, 2018, 2019, 2020], geography_type=’tract’,

)

__init__(store=None, dataset='acs5')[source]
Parameters:
  • store (Optional['TemporalDataStore'])

  • dataset (str)

Return type:

None

build(variables, years, geography_type, snapshots_df=None)[source]

Build time-series schemas from snapshot data.

Parameters:
  • variables (list[str]) – Census variable codes (e.g. [‘B01001_001E’])

  • years (list[int]) – Years to include in the series

  • geography_type (str) – Geography level name

  • snapshots_df (Optional['pd.DataFrame']) – Pre-loaded snapshots DataFrame with columns: boundary_id, year, values (dict column), moe_values (dict column). If None, loads from self.store.

Returns:

List of TimeseriesBuildResult (one per variable), with created DemographicTimeSeriesSchema instances accessible via result.series.

Return type:

list[TimeseriesBuildResult]

class siege_utilities.geo.temporal.services.TemporalDemographicService[source]

Bases: object

Build demographic snapshots from Census API data.

Converts raw Census API DataFrames into DemographicSnapshotSchema instances and optionally persists them via TemporalDataStore.

__init__(store=None, api_key=None)[source]
Parameters:
  • store (Optional['TemporalDataStore'])

  • api_key (str | None)

Return type:

None

build_snapshots(df, geography_type, year, dataset='acs5', geoid_column='GEOID', variable_columns=None)[source]

Convert a Census API DataFrame to snapshot schema instances.

Parameters:
  • df (pd.DataFrame) – DataFrame from Census API (columns: GEOID + variable codes)

  • geography_type (str) – Geography level name

  • year (int) – Census/ACS year

  • dataset (str) – Census dataset

  • geoid_column (str) – Column containing GEOIDs

  • variable_columns (list[str] | None) – Explicit list of variable columns. If None, uses all columns except geoid_column and ‘NAME’.

Returns:

List of DemographicSnapshotSchema instances

Return type:

list[DemographicSnapshotSchema]

Pure-Python persistence for temporal boundaries and demographics.

Storage layout (Parquet default):

{root}/boundaries/{geography_type}/{vintage_year}.parquet {root}/demographics/{geography_type}/{dataset}_{year}.parquet {root}/timeseries/{geography_type}/{variable_code}_{dataset}.parquet

GeoPackage is available as an alternative via format=”gpkg” (boundaries only).

class siege_utilities.geo.temporal.store.TemporalDataStore[source]

Bases: object

Pure-Python persistence for temporal geographic data.

Follows the CrosswalkClient pattern: class with module-level convenience functions for the common case.

SUPPORTED_FORMATS = ('parquet', 'gpkg')
__init__(root_dir=None, format='parquet')[source]
Parameters:
Return type:

None

save_boundaries(gdf, geography_type, vintage_year)[source]

Persist a GeoDataFrame of boundaries.

Parameters:
  • gdf (gpd.GeoDataFrame)

  • geography_type (str)

  • vintage_year (int)

Return type:

Path

load_boundaries(geography_type, vintage_year, state_fips=None)[source]

Load boundaries for a geography type and vintage year.

Parameters:
  • geography_type (str)

  • vintage_year (int)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

query_boundaries_at_date(geography_type, query_date, state_fips=None)[source]

Load the best-matching vintage for a given date.

If boundaries have valid_from/valid_to columns, those are checked. Otherwise, the nearest vintage_year <= query_date.year is used.

Parameters:
  • geography_type (str)

  • query_date (date)

  • state_fips (str | None)

Return type:

gpd.GeoDataFrame

list_available_vintages(geography_type)[source]

List vintage years available on disk for a geography type.

Parameters:

geography_type (str)

Return type:

list[int]

save_demographics(snapshots, geography_type, dataset='acs5', year=None)[source]

Persist demographic snapshot data as Parquet.

Parameters:
  • snapshots (pd.DataFrame)

  • geography_type (str)

  • dataset (str)

  • year (int | None)

Return type:

Path

load_demographics(geography_type, year=None, dataset='acs5')[source]

Load demographic data. If year is None, loads all available years.

Parameters:
  • geography_type (str)

  • year (int | None)

  • dataset (str)

Return type:

pd.DataFrame

save_timeseries(series, geography_type, variable_code=None, dataset='acs5')[source]

Persist pre-computed time-series data as Parquet.

Parameters:
  • series (pd.DataFrame)

  • geography_type (str)

  • variable_code (str | None)

  • dataset (str)

Return type:

Path

load_timeseries(geography_type, variable_code, dataset='acs5')[source]

Load pre-computed time-series data.

Parameters:
  • geography_type (str)

  • variable_code (str)

  • dataset (str)

Return type:

pd.DataFrame

siege_utilities.geo.temporal.store.get_temporal_store(root_dir=None, format='parquet')[source]

Get or create the singleton TemporalDataStore.

Parameters:
Return type:

TemporalDataStore

siege_utilities.geo.temporal.store.save_boundaries(gdf, geography_type, vintage_year, **kwargs)[source]

Save boundaries via the default store.

Parameters:
  • gdf (gpd.GeoDataFrame) – GeoDataFrame of boundaries to persist.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.temporal.store.load_boundaries(geography_type, vintage_year, state_fips=None, **kwargs)[source]

Load boundaries via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • vintage_year (int) – Census vintage year.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.store.query_boundaries_at_date(geography_type, query_date, state_fips=None, **kwargs)[source]

Query boundaries at a date via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • query_date (date) – Date for which to find matching boundaries.

  • state_fips (str | None) – Optional state FIPS code to filter results.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

gpd.GeoDataFrame

siege_utilities.geo.temporal.store.save_demographics(snapshots, geography_type, dataset='acs5', year=None, **kwargs)[source]

Save demographics via the default store.

Parameters:
  • snapshots (pd.DataFrame) – DataFrame of demographic snapshot data.

  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • dataset (str) – Dataset identifier (default “acs5”).

  • year (int | None) – Year of the snapshot. Inferred from DataFrame if omitted.

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

Path

siege_utilities.geo.temporal.store.load_demographics(geography_type, year=None, dataset='acs5', **kwargs)[source]

Load demographics via the default store.

Parameters:
  • geography_type (str) – Type of geography (e.g. “county”, “tract”).

  • year (int | None) – Year to load. If None, loads all available years.

  • dataset (str) – Dataset identifier (default “acs5”).

  • **kwargs – Forwarded to get_temporal_store(). Accepts root_dir and format.

Return type:

pd.DataFrame

Time-series analysis for Census longitudinal data.

This module provides tools for analyzing Census data across multiple years, including change metrics, trend classification, and multi-year data fetching.

Key features: - Fetch data for multiple years with automatic boundary normalization - Calculate change metrics (absolute, percent, CAGR) - Classify trends into categories (rapid growth, stable, decline, etc.) - Support for various analysis approaches

Example usage:
from siege_utilities.geo.timeseries import (

get_longitudinal_data, calculate_change_metrics, classify_trends

)

# Fetch income data for multiple years df = get_longitudinal_data(

variables=’B19013_001E’, years=[2010, 2015, 2020], geography=’tract’, state=’California’, target_year=2020 # Normalize all to 2020 boundaries

)

# Calculate change metrics df = calculate_change_metrics(

df=df, value_column=’B19013_001E’, start_year=2010, end_year=2020, metrics=[‘absolute’, ‘percent’, ‘cagr’]

)

# Classify trends df = classify_trends(

df=df, change_column=’B19013_001E_pct_change_2010_2020’

)

siege_utilities.geo.timeseries.get_longitudinal_data(variables, years, geography='tract', state=None, county_fips=None, target_year=2020, normalize_boundaries=True, include_moe=False, include_geometry=False, dataset='acs5', strict_years=True)[source]

Fetch Census data for multiple years and return in wide format.

This function: 1. Fetches data for each requested year 2. Optionally normalizes all data to a consistent boundary year 3. Merges into a wide-format DataFrame with year-suffixed columns

Parameters:
  • variables (str | List[str]) – Census variable code(s) or predefined group name (e.g., ‘B19013_001E’, [‘B19013_001E’, ‘B01001_001E’], or ‘income’)

  • years (List[int]) – List of years to fetch data for

  • geography (str) – Geographic level (‘state’, ‘county’, ‘tract’, ‘block_group’)

  • state (str | None) – State identifier (name, abbreviation, or FIPS code)

  • county_fips (str | None) – County FIPS code (3 digits) for filtering

  • target_year (int) – Year to normalize boundaries to (default: 2020)

  • normalize_boundaries (bool) – If True, apply crosswalks to normalize all years to target_year boundaries. Required for accurate comparisons.

  • include_moe (bool) – Include margin of error columns

  • include_geometry (bool) – If True, include geometry from target year

  • dataset (str) – Census dataset (‘acs5’, ‘acs1’)

  • strict_years (bool) – If True (default), raise ValueError when any requested year is unavailable or fails to fetch. If False, log warnings and continue with available years.

Returns:

  • GEOID: Geography identifier (in target_year vintage if normalized)

  • NAME: Geography name (from most recent year)

  • {variable}_{year}: Variable value for each year

Return type:

Wide-format DataFrame with columns

Example

# Get income data for LA County tracts 2015-2020 df = get_longitudinal_data(

variables=’B19013_001E’, years=[2015, 2017, 2020], geography=’tract’, state=’CA’, county_fips=’037’

)

# Columns: GEOID, NAME, B19013_001E_2015, B19013_001E_2017, B19013_001E_2020

siege_utilities.geo.timeseries.get_available_years(dataset='acs5', variable=None)[source]

Get list of available years for a Census dataset.

Parameters:
  • dataset (str) – Census dataset (‘acs5’, ‘acs1’, ‘dec’)

  • variable (str | None) – Optional variable to check availability for

Returns:

List of available years

Return type:

List[int]

siege_utilities.geo.timeseries.get_available_survey_years(dataset='acs5', variable=None)

Get list of available years for a Census dataset.

Parameters:
  • dataset (str) – Census dataset (‘acs5’, ‘acs1’, ‘dec’)

  • variable (str | None) – Optional variable to check availability for

Returns:

List of available years

Return type:

List[int]

siege_utilities.geo.timeseries.validate_longitudinal_years(years, dataset='acs5')[source]

Validate and filter years for longitudinal analysis.

Parameters:
  • years (List[int]) – Requested years

  • dataset (str) – Census dataset

Returns:

List of valid years

Raises:

ValueError – If no valid years

Return type:

List[int]

siege_utilities.geo.timeseries.calculate_change_metrics(df, value_column, start_year=None, end_year=None, metrics=None)[source]

Calculate change metrics for a variable across time.

This function expects wide-format data with year-suffixed columns (e.g., B19013_001E_2010, B19013_001E_2020).

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name (without year suffix)

  • start_year (int | None) – Start year for comparison. If None, uses earliest year found.

  • end_year (int | None) – End year for comparison. If None, uses latest year found.

  • metrics (List[str]) – List of metrics to calculate. Options: - ‘absolute’: Raw change (end - start) - ‘percent’: Percentage change ((end - start) / start * 100) - ‘cagr’: Compound Annual Growth Rate - ‘annualized’: Annualized change (absolute change / years) Default: [‘absolute’, ‘percent’, ‘cagr’]

Returns:

  • {value_column}_change_{start}_{end}: Absolute change

  • {value_column}_pct_change_{start}_{end}: Percentage change

  • {value_column}_cagr_{start}_{end}: CAGR

Return type:

DataFrame with original columns plus new metric columns

Example

df = calculate_change_metrics(

df=income_data, value_column=’B19013_001E’, start_year=2010, end_year=2020, metrics=[‘absolute’, ‘percent’, ‘cagr’]

)

siege_utilities.geo.timeseries.calculate_multi_period_changes(df, value_column, metrics=None)[source]

Calculate change metrics between all consecutive periods.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • metrics (List[str]) – Metrics to calculate

Returns:

DataFrame with changes between each pair of consecutive years

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.calculate_index(df, value_column, base_year=None, index_value=100.0)[source]

Calculate an index relative to a base year.

This is useful for comparing changes across geographies with different absolute values.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • base_year (int | None) – Year to use as index base. If None, uses earliest year.

  • index_value (float) – Value to set base year to (default: 100)

Returns:

DataFrame with indexed columns (e.g., B19013_001E_index_2010)

Return type:

pandas.DataFrame

Example

df = calculate_index(df, ‘B19013_001E’, base_year=2010) # Creates columns: B19013_001E_index_2010, B19013_001E_index_2015, …

siege_utilities.geo.timeseries.get_change_summary(df, value_column, start_year=None, end_year=None)[source]

Get summary statistics for changes in a variable.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • start_year (int | None) – Start year

  • end_year (int | None) – End year

Returns:

Dictionary with summary statistics

Return type:

dict

class siege_utilities.geo.timeseries.TrendCategory[source]

Bases: Enum

Standard trend categories for classification.

RAPID_GROWTH = 'rapid_growth'
MODERATE_GROWTH = 'moderate_growth'
STABLE = 'stable'
MODERATE_DECLINE = 'moderate_decline'
RAPID_DECLINE = 'rapid_decline'
class siege_utilities.geo.timeseries.TrendThresholds[source]

Bases: object

Threshold configuration for trend classification.

rapid_growth

Threshold for rapid growth (values >= this)

Type:

float

moderate_growth

Threshold for moderate growth

Type:

float

stable_lower

Lower bound for stable category

Type:

float

moderate_decline

Threshold for moderate decline

Type:

float

rapid_decline

Values below this (or moderate_decline if None)

rapid_growth: float = 20.0
moderate_growth: float = 5.0
stable_lower: float = -5.0
moderate_decline: float = -20.0
to_dict()[source]

Convert to dictionary for display.

Return type:

Dict[str, float]

__init__(rapid_growth=20.0, moderate_growth=5.0, stable_lower=-5.0, moderate_decline=-20.0)
Parameters:
Return type:

None

Classify changes into trend categories.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values (typically percentage change)

  • thresholds (TrendThresholds | str | Dict[str, float] | None) – Threshold configuration. Can be: - TrendThresholds object - Preset name (‘default’, ‘conservative’, ‘sensitive’, ‘housing’, ‘population’) - Dict with keys: rapid_growth, moderate_growth, stable_lower, moderate_decline - None (uses default thresholds)

  • output_column (str) – Name for the category column

Returns:

DataFrame with new trend_category column

Return type:

pandas.DataFrame

Example

df = classify_trends(

df=income_changes, change_column=’B19013_001E_pct_change_2010_2020’, thresholds=’conservative’

)

siege_utilities.geo.timeseries.classify_by_zscore(df, change_column, output_column='trend_zscore_category')[source]

Classify trends using z-scores (statistical approach).

This method classifies based on how many standard deviations a geography’s change is from the mean, providing a relative classification within the dataset.

Categories: - extreme_positive: z >= 2 - high_positive: 1 <= z < 2 - average: -1 < z < 1 - high_negative: -2 < z <= -1 - extreme_negative: z <= -2

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • output_column (str) – Name for the category column

Returns:

DataFrame with z-score category column and z-score values

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.classify_by_quantiles(df, change_column, n_quantiles=5, labels=None, output_column='trend_quantile')[source]

Classify trends into quantile-based categories.

This method divides the data into equal-sized groups (quintiles, deciles, etc.) which is useful when you want balanced category sizes.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • n_quantiles (int) – Number of quantiles (default: 5 for quintiles)

  • labels (List[str] | None) – Custom labels for quantiles. If None, uses: - 5 quantiles: [‘lowest’, ‘low’, ‘middle’, ‘high’, ‘highest’] - Other: numeric labels 1 to n

  • output_column (str) – Name for the category column

Returns:

DataFrame with quantile category column

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.get_trend_summary(df, trend_column='trend_category', geoid_column='GEOID')[source]

Get summary statistics for trend classification.

Parameters:
  • df (pandas.DataFrame) – DataFrame with trend categories

  • trend_column (str) – Name of the trend category column

  • geoid_column (str) – Name of the GEOID column

Returns:

Dictionary with summary statistics

Return type:

Dict

siege_utilities.geo.timeseries.identify_outliers(df, change_column, method='iqr', threshold=1.5)[source]

Identify outlier geographies based on change values.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • method (str) – Outlier detection method: - ‘iqr’: Interquartile range method - ‘zscore’: Z-score method

  • threshold (float) – Threshold for outlier detection: - For IQR: multiplier (default 1.5) - For zscore: number of standard deviations (default 1.5)

Returns:

DataFrame with is_outlier and outlier_type columns

Return type:

pandas.DataFrame

Compare trend classifications between two DataFrames.

Useful for comparing trend patterns between different: - Time periods - Variables - Geographic subsets

Parameters:
  • df1 (pandas.DataFrame) – First DataFrame with trend classification

  • df2 (pandas.DataFrame) – Second DataFrame with trend classification

  • trend_column (str) – Name of trend category column

  • geoid_column (str) – Name of GEOID column

Returns:

DataFrame with comparison columns

Return type:

pandas.DataFrame

class siege_utilities.geo.timeseries.BoundaryStabilityMetrics[source]

Bases: object

Stability metrics for a set of boundaries between two vintages.

source_year

Earlier Census vintage.

Type:

int

target_year

Later Census vintage.

Type:

int

geography_type

Geography level (tract, county, etc.).

Type:

str

total_source

Number of source boundaries.

Type:

int

total_target

Number of target boundaries.

Type:

int

unchanged

Boundaries with weight >= threshold (effectively identical).

Type:

int

split

Source boundaries that map to multiple targets.

Type:

int

merged

Target boundaries that receive from multiple sources.

Type:

int

net_change

target_count - source_count.

Type:

int

pct_unchanged

Fraction of source boundaries unchanged.

Type:

float

churn_rate

1 - pct_unchanged.

Type:

float

source_year: int
target_year: int
geography_type: str
total_source: int = 0
total_target: int = 0
unchanged: int = 0
split: int = 0
merged: int = 0
net_change: int = 0
pct_unchanged: float = 0.0
churn_rate: float = 0.0
__init__(source_year, target_year, geography_type, total_source=0, total_target=0, unchanged=0, split=0, merged=0, net_change=0, pct_unchanged=0.0, churn_rate=0.0)
Parameters:
  • source_year (int)

  • target_year (int)

  • geography_type (str)

  • total_source (int)

  • total_target (int)

  • unchanged (int)

  • split (int)

  • merged (int)

  • net_change (int)

  • pct_unchanged (float)

  • churn_rate (float)

Return type:

None

class siege_utilities.geo.timeseries.AllocationEfficiencyMetrics[source]

Bases: object

Metrics describing how cleanly crosswalk weights allocate values.

mean_weight

Average crosswalk weight across all relationships.

Type:

float

median_weight

Median weight.

Type:

float

std_weight

Standard deviation of weights.

Type:

float

pct_above_90

Fraction of relationships with weight > 0.9.

Type:

float

pct_above_50

Fraction of relationships with weight > 0.5.

Type:

float

pct_below_10

Fraction of relationships with weight < 0.1.

Type:

float

n_relationships

Total number of crosswalk relationships.

Type:

int

mean_weight: float = 0.0
median_weight: float = 0.0
std_weight: float = 0.0
pct_above_90: float = 0.0
pct_above_50: float = 0.0
pct_below_10: float = 0.0
n_relationships: int = 0
__init__(mean_weight=0.0, median_weight=0.0, std_weight=0.0, pct_above_90=0.0, pct_above_50=0.0, pct_below_10=0.0, n_relationships=0)
Parameters:
Return type:

None

Bases: object

A single step in a reallocation chain.

source_geoid

Source boundary GEOID.

Type:

str

target_geoid

Target boundary GEOID.

Type:

str

source_year

Source vintage year.

Type:

int

target_year

Target vintage year.

Type:

int

weight

Allocation weight for this link.

Type:

float

relationship

IDENTICAL, SPLIT, MERGED, PARTIAL, RENAMED.

Type:

str

source_geoid: str
target_geoid: str
source_year: int
target_year: int
weight: float
relationship: str
__init__(source_geoid, target_geoid, source_year, target_year, weight, relationship)
Parameters:
  • source_geoid (str)

  • target_geoid (str)

  • source_year (int)

  • target_year (int)

  • weight (float)

  • relationship (str)

Return type:

None

class siege_utilities.geo.timeseries.ReallocationChain[source]

Bases: object

A complete chain tracking one GEOID through multiple vintages.

origin_geoid

Starting GEOID.

Type:

str

origin_year

Starting vintage year.

Type:

int

Ordered list of chain links.

Type:

list[siege_utilities.geo.timeseries.crosswalk_analytics.ChainLink]

terminal_geoids

Final GEOIDs at the end of the chain.

Type:

list[str]

cumulative_weight

Product of weights along the chain (data quality).

Type:

float

origin_geoid: str
origin_year: int
links: list[ChainLink]
terminal_geoids: list[str]
cumulative_weight: float = 1.0
__init__(origin_geoid, origin_year, links=<factory>, terminal_geoids=<factory>, cumulative_weight=1.0)
Parameters:
Return type:

None

siege_utilities.geo.timeseries.compute_boundary_stability(crosswalk_df, source_year, target_year, geography_type='tract', unchanged_threshold=0.999)[source]

Compute stability metrics for boundaries between two vintages.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame with columns: source_geoid, target_geoid, weight.

  • source_year (int) – Earlier vintage year.

  • target_year (int) – Later vintage year.

  • geography_type (str) – Geography level name.

  • unchanged_threshold (float) – Weight threshold to consider a boundary unchanged.

Returns:

BoundaryStabilityMetrics with counts and rates.

Return type:

BoundaryStabilityMetrics

siege_utilities.geo.timeseries.compute_allocation_efficiency(crosswalk_df)[source]

Compute metrics describing crosswalk weight distribution.

High-quality crosswalks have most weights near 1.0 (clean mappings). Low-quality crosswalks have many small weights (fragmented boundaries).

Parameters:

crosswalk_df (pandas.DataFrame) – DataFrame with a weight column.

Returns:

AllocationEfficiencyMetrics.

Return type:

AllocationEfficiencyMetrics

siege_utilities.geo.timeseries.build_reallocation_chain(crosswalk_dfs, origin_geoid, min_weight=0.01)[source]

Track a GEOID through multiple vintage transitions.

Given a sequence of crosswalk DataFrames (one per transition), follows the origin GEOID forward through each step, accumulating weights.

Parameters:
  • crosswalk_dfs (list[tuple[int, int, pandas.DataFrame]]) – List of (source_year, target_year, crosswalk_df) tuples, ordered chronologically.

  • origin_geoid (str) – Starting GEOID to track.

  • min_weight (float) – Minimum cumulative weight to continue tracking.

Returns:

ReallocationChain with links and terminal GEOIDs.

Return type:

ReallocationChain

siege_utilities.geo.timeseries.compare_vintage_stability(crosswalk_dfs, geography_type='tract')[source]

Compare boundary stability across multiple vintage transitions.

Parameters:
Returns:

DataFrame with one row per transition and stability metrics.

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.identify_volatile_boundaries(crosswalk_df, threshold=0.5, top_n=None)[source]

Identify source boundaries with the most fragmented mappings.

“Volatile” boundaries are those that split into many targets with small weights, indicating significant boundary changes.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame with crosswalk relationships.

  • threshold (float) – Maximum weight to consider “fragmented” (default 0.5).

  • top_n (int | None) – Return only the top N most volatile (None for all).

Returns:

source_geoid, n_targets, max_weight, min_weight, mean_weight, total_weight.

Return type:

DataFrame with columns

Change metrics calculations for Census time-series analysis.

This module provides functions to calculate various change metrics from longitudinal Census data, including:

  • Absolute change

  • Percentage change

  • Compound Annual Growth Rate (CAGR)

  • Per-period change rates

Example usage:

from siege_utilities.geo.timeseries import calculate_change_metrics

# Calculate metrics for income data df = calculate_change_metrics(

df=longitudinal_data, value_column=’B19013_001E’, metrics=[‘absolute’, ‘percent’, ‘cagr’]

)

siege_utilities.geo.timeseries.change_metrics.calculate_change_metrics(df, value_column, start_year=None, end_year=None, metrics=None)[source]

Calculate change metrics for a variable across time.

This function expects wide-format data with year-suffixed columns (e.g., B19013_001E_2010, B19013_001E_2020).

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name (without year suffix)

  • start_year (int | None) – Start year for comparison. If None, uses earliest year found.

  • end_year (int | None) – End year for comparison. If None, uses latest year found.

  • metrics (List[str]) – List of metrics to calculate. Options: - ‘absolute’: Raw change (end - start) - ‘percent’: Percentage change ((end - start) / start * 100) - ‘cagr’: Compound Annual Growth Rate - ‘annualized’: Annualized change (absolute change / years) Default: [‘absolute’, ‘percent’, ‘cagr’]

Returns:

  • {value_column}_change_{start}_{end}: Absolute change

  • {value_column}_pct_change_{start}_{end}: Percentage change

  • {value_column}_cagr_{start}_{end}: CAGR

Return type:

DataFrame with original columns plus new metric columns

Example

df = calculate_change_metrics(

df=income_data, value_column=’B19013_001E’, start_year=2010, end_year=2020, metrics=[‘absolute’, ‘percent’, ‘cagr’]

)

siege_utilities.geo.timeseries.change_metrics.calculate_index(df, value_column, base_year=None, index_value=100.0)[source]

Calculate an index relative to a base year.

This is useful for comparing changes across geographies with different absolute values.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • base_year (int | None) – Year to use as index base. If None, uses earliest year.

  • index_value (float) – Value to set base year to (default: 100)

Returns:

DataFrame with indexed columns (e.g., B19013_001E_index_2010)

Return type:

pandas.DataFrame

Example

df = calculate_index(df, ‘B19013_001E’, base_year=2010) # Creates columns: B19013_001E_index_2010, B19013_001E_index_2015, …

siege_utilities.geo.timeseries.change_metrics.calculate_multi_period_changes(df, value_column, metrics=None)[source]

Calculate change metrics between all consecutive periods.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • metrics (List[str]) – Metrics to calculate

Returns:

DataFrame with changes between each pair of consecutive years

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.change_metrics.get_change_summary(df, value_column, start_year=None, end_year=None)[source]

Get summary statistics for changes in a variable.

Parameters:
  • df (pandas.DataFrame) – DataFrame with year-suffixed columns

  • value_column (str) – Base variable name

  • start_year (int | None) – Start year

  • end_year (int | None) – End year

Returns:

Dictionary with summary statistics

Return type:

dict

Time-series analytics for geographic crosswalk data.

Analyzes how boundary relationships evolve across Census vintages: - Allocation efficiency: how cleanly do source boundaries map to targets? - Boundary stability: which geographies change the most between vintages? - Reallocation chains: tracking a GEOID through multiple vintage transitions.

Builds on TemporalCrosswalk model data and the crosswalk module.

class siege_utilities.geo.timeseries.crosswalk_analytics.AllocationEfficiencyMetrics[source]

Bases: object

Metrics describing how cleanly crosswalk weights allocate values.

mean_weight

Average crosswalk weight across all relationships.

Type:

float

median_weight

Median weight.

Type:

float

std_weight

Standard deviation of weights.

Type:

float

pct_above_90

Fraction of relationships with weight > 0.9.

Type:

float

pct_above_50

Fraction of relationships with weight > 0.5.

Type:

float

pct_below_10

Fraction of relationships with weight < 0.1.

Type:

float

n_relationships

Total number of crosswalk relationships.

Type:

int

mean_weight: float = 0.0
median_weight: float = 0.0
std_weight: float = 0.0
pct_above_90: float = 0.0
pct_above_50: float = 0.0
pct_below_10: float = 0.0
n_relationships: int = 0
__init__(mean_weight=0.0, median_weight=0.0, std_weight=0.0, pct_above_90=0.0, pct_above_50=0.0, pct_below_10=0.0, n_relationships=0)
Parameters:
Return type:

None

class siege_utilities.geo.timeseries.crosswalk_analytics.BoundaryStabilityMetrics[source]

Bases: object

Stability metrics for a set of boundaries between two vintages.

source_year

Earlier Census vintage.

Type:

int

target_year

Later Census vintage.

Type:

int

geography_type

Geography level (tract, county, etc.).

Type:

str

total_source

Number of source boundaries.

Type:

int

total_target

Number of target boundaries.

Type:

int

unchanged

Boundaries with weight >= threshold (effectively identical).

Type:

int

split

Source boundaries that map to multiple targets.

Type:

int

merged

Target boundaries that receive from multiple sources.

Type:

int

net_change

target_count - source_count.

Type:

int

pct_unchanged

Fraction of source boundaries unchanged.

Type:

float

churn_rate

1 - pct_unchanged.

Type:

float

source_year: int
target_year: int
geography_type: str
total_source: int = 0
total_target: int = 0
unchanged: int = 0
split: int = 0
merged: int = 0
net_change: int = 0
pct_unchanged: float = 0.0
churn_rate: float = 0.0
__init__(source_year, target_year, geography_type, total_source=0, total_target=0, unchanged=0, split=0, merged=0, net_change=0, pct_unchanged=0.0, churn_rate=0.0)
Parameters:
  • source_year (int)

  • target_year (int)

  • geography_type (str)

  • total_source (int)

  • total_target (int)

  • unchanged (int)

  • split (int)

  • merged (int)

  • net_change (int)

  • pct_unchanged (float)

  • churn_rate (float)

Return type:

None

Bases: object

A single step in a reallocation chain.

source_geoid

Source boundary GEOID.

Type:

str

target_geoid

Target boundary GEOID.

Type:

str

source_year

Source vintage year.

Type:

int

target_year

Target vintage year.

Type:

int

weight

Allocation weight for this link.

Type:

float

relationship

IDENTICAL, SPLIT, MERGED, PARTIAL, RENAMED.

Type:

str

source_geoid: str
target_geoid: str
source_year: int
target_year: int
weight: float
relationship: str
__init__(source_geoid, target_geoid, source_year, target_year, weight, relationship)
Parameters:
  • source_geoid (str)

  • target_geoid (str)

  • source_year (int)

  • target_year (int)

  • weight (float)

  • relationship (str)

Return type:

None

class siege_utilities.geo.timeseries.crosswalk_analytics.ReallocationChain[source]

Bases: object

A complete chain tracking one GEOID through multiple vintages.

origin_geoid

Starting GEOID.

Type:

str

origin_year

Starting vintage year.

Type:

int

Ordered list of chain links.

Type:

list[siege_utilities.geo.timeseries.crosswalk_analytics.ChainLink]

terminal_geoids

Final GEOIDs at the end of the chain.

Type:

list[str]

cumulative_weight

Product of weights along the chain (data quality).

Type:

float

origin_geoid: str
origin_year: int
links: list[ChainLink]
terminal_geoids: list[str]
cumulative_weight: float = 1.0
__init__(origin_geoid, origin_year, links=<factory>, terminal_geoids=<factory>, cumulative_weight=1.0)
Parameters:
Return type:

None

siege_utilities.geo.timeseries.crosswalk_analytics.build_reallocation_chain(crosswalk_dfs, origin_geoid, min_weight=0.01)[source]

Track a GEOID through multiple vintage transitions.

Given a sequence of crosswalk DataFrames (one per transition), follows the origin GEOID forward through each step, accumulating weights.

Parameters:
  • crosswalk_dfs (list[tuple[int, int, pandas.DataFrame]]) – List of (source_year, target_year, crosswalk_df) tuples, ordered chronologically.

  • origin_geoid (str) – Starting GEOID to track.

  • min_weight (float) – Minimum cumulative weight to continue tracking.

Returns:

ReallocationChain with links and terminal GEOIDs.

Return type:

ReallocationChain

siege_utilities.geo.timeseries.crosswalk_analytics.compare_vintage_stability(crosswalk_dfs, geography_type='tract')[source]

Compare boundary stability across multiple vintage transitions.

Parameters:
Returns:

DataFrame with one row per transition and stability metrics.

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.crosswalk_analytics.compute_allocation_efficiency(crosswalk_df)[source]

Compute metrics describing crosswalk weight distribution.

High-quality crosswalks have most weights near 1.0 (clean mappings). Low-quality crosswalks have many small weights (fragmented boundaries).

Parameters:

crosswalk_df (pandas.DataFrame) – DataFrame with a weight column.

Returns:

AllocationEfficiencyMetrics.

Return type:

AllocationEfficiencyMetrics

siege_utilities.geo.timeseries.crosswalk_analytics.compute_boundary_stability(crosswalk_df, source_year, target_year, geography_type='tract', unchanged_threshold=0.999)[source]

Compute stability metrics for boundaries between two vintages.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame with columns: source_geoid, target_geoid, weight.

  • source_year (int) – Earlier vintage year.

  • target_year (int) – Later vintage year.

  • geography_type (str) – Geography level name.

  • unchanged_threshold (float) – Weight threshold to consider a boundary unchanged.

Returns:

BoundaryStabilityMetrics with counts and rates.

Return type:

BoundaryStabilityMetrics

siege_utilities.geo.timeseries.crosswalk_analytics.identify_volatile_boundaries(crosswalk_df, threshold=0.5, top_n=None)[source]

Identify source boundaries with the most fragmented mappings.

“Volatile” boundaries are those that split into many targets with small weights, indicating significant boundary changes.

Parameters:
  • crosswalk_df (pandas.DataFrame) – DataFrame with crosswalk relationships.

  • threshold (float) – Maximum weight to consider “fragmented” (default 0.5).

  • top_n (int | None) – Return only the top N most volatile (None for all).

Returns:

source_geoid, n_targets, max_weight, min_weight, mean_weight, total_weight.

Return type:

DataFrame with columns

Longitudinal data fetching for Census time-series analysis.

This module provides functions to fetch Census data across multiple years and normalize them to a consistent boundary vintage for time-series analysis.

It integrates with: - CensusAPIClient for fetching demographic data - Crosswalk module for boundary normalization

Example usage:

from siege_utilities.geo.timeseries import get_longitudinal_data

# Fetch median income for California tracts 2010-2020 df = get_longitudinal_data(

variables=’B19013_001E’, years=[2010, 2015, 2020], geography=’tract’, state=’California’, target_year=2020

)

# Result has columns: GEOID, B19013_001E_2010, B19013_001E_2015, B19013_001E_2020

# Multi-decade alignment with inflation adjustment from siege_utilities.geo.timeseries.longitudinal_data import LongitudinalAligner

aligner = LongitudinalAligner(target_vintage=2020) aligned = aligner.align(df_2010, source_vintage=2010, geography=’tract’) adjusted = aligner.adjust_inflation(aligned, dollar_columns=[‘B19013_001E’],

from_year=2010, to_year=2020)

class siege_utilities.geo.timeseries.longitudinal_data.AlignmentResult[source]

Bases: object

Result of a longitudinal alignment operation.

data

Aligned DataFrame with GEOIDs in the target vintage.

Type:

pandas.DataFrame

source_vintage

Original boundary vintage year.

Type:

int

target_vintage

Target boundary vintage year.

Type:

int

method

Method used for alignment ('crosswalk' or 'areal').

Type:

str

rows_before

Row count before alignment.

Type:

int

rows_after

Row count after alignment.

Type:

int

warnings

Any warnings generated during alignment.

Type:

List[str]

data: pandas.DataFrame
source_vintage: int
target_vintage: int
method: str = 'crosswalk'
rows_before: int = 0
rows_after: int = 0
warnings: List[str]
__init__(data, source_vintage, target_vintage, method='crosswalk', rows_before=0, rows_after=0, warnings=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.timeseries.longitudinal_data.LongitudinalAligner[source]

Bases: object

Align Census data across vintage years with optional inflation adjustment.

Chains crosswalks for multi-decade transitions (e.g. 2000→2010→2020) and falls back to areal interpolation when crosswalk data is unavailable.

Parameters:
  • target_vintage (int) – Target boundary vintage year (2000, 2010, or 2020).

  • geography (str) – Geographic level ('tract', 'county', 'block_group').

  • state_fips (str, optional) – Two-digit state FIPS to limit crosswalk scope.

Example

aligner = LongitudinalAligner(target_vintage=2020)
result = aligner.align(df_2010, source_vintage=2010, geography='tract')
adjusted = aligner.adjust_inflation(
    result.data, dollar_columns=['B19013_001E'],
    from_year=2010, to_year=2020,
)
__init__(target_vintage=2020, geography='tract', state_fips=None)[source]
Parameters:
  • target_vintage (int)

  • geography (str)

  • state_fips (str | None)

align(df, source_vintage, geography=None, state_fips=None, geoid_column='GEOID')[source]

Align df from source_vintage boundaries to target_vintage.

Parameters:
  • df (DataFrame) – Data with a GEOID column in source_vintage boundaries.

  • source_vintage (int) – Boundary vintage of df (2000, 2010, or 2020).

  • geography (str, optional) – Override instance geography.

  • state_fips (str, optional) – Override instance state_fips.

  • geoid_column (str) – Name of the GEOID column in df.

Return type:

AlignmentResult

static adjust_inflation(df, dollar_columns, from_year, to_year)[source]

Adjust dollar-denominated columns for inflation using CPI-U.

Parameters:
  • df (DataFrame) – Data containing dollar-valued columns.

  • dollar_columns (list of str) – Column names to adjust.

  • from_year (int) – Base year of the dollar values.

  • to_year (int) – Target year to express values in.

Return type:

DataFrame with adjusted values (original columns overwritten).

siege_utilities.geo.timeseries.longitudinal_data.get_available_years(dataset='acs5', variable=None)[source]

Get list of available years for a Census dataset.

Parameters:
  • dataset (str) – Census dataset (‘acs5’, ‘acs1’, ‘dec’)

  • variable (str | None) – Optional variable to check availability for

Returns:

List of available years

Return type:

List[int]

siege_utilities.geo.timeseries.longitudinal_data.get_longitudinal_data(variables, years, geography='tract', state=None, county_fips=None, target_year=2020, normalize_boundaries=True, include_moe=False, include_geometry=False, dataset='acs5', strict_years=True)[source]

Fetch Census data for multiple years and return in wide format.

This function: 1. Fetches data for each requested year 2. Optionally normalizes all data to a consistent boundary year 3. Merges into a wide-format DataFrame with year-suffixed columns

Parameters:
  • variables (str | List[str]) – Census variable code(s) or predefined group name (e.g., ‘B19013_001E’, [‘B19013_001E’, ‘B01001_001E’], or ‘income’)

  • years (List[int]) – List of years to fetch data for

  • geography (str) – Geographic level (‘state’, ‘county’, ‘tract’, ‘block_group’)

  • state (str | None) – State identifier (name, abbreviation, or FIPS code)

  • county_fips (str | None) – County FIPS code (3 digits) for filtering

  • target_year (int) – Year to normalize boundaries to (default: 2020)

  • normalize_boundaries (bool) – If True, apply crosswalks to normalize all years to target_year boundaries. Required for accurate comparisons.

  • include_moe (bool) – Include margin of error columns

  • include_geometry (bool) – If True, include geometry from target year

  • dataset (str) – Census dataset (‘acs5’, ‘acs1’)

  • strict_years (bool) – If True (default), raise ValueError when any requested year is unavailable or fails to fetch. If False, log warnings and continue with available years.

Returns:

  • GEOID: Geography identifier (in target_year vintage if normalized)

  • NAME: Geography name (from most recent year)

  • {variable}_{year}: Variable value for each year

Return type:

Wide-format DataFrame with columns

Example

# Get income data for LA County tracts 2015-2020 df = get_longitudinal_data(

variables=’B19013_001E’, years=[2015, 2017, 2020], geography=’tract’, state=’CA’, county_fips=’037’

)

# Columns: GEOID, NAME, B19013_001E_2015, B19013_001E_2017, B19013_001E_2020

siege_utilities.geo.timeseries.longitudinal_data.inflation_factor(from_year, to_year)[source]

Return the CPI-U multiplier to convert dollars from from_year to to_year.

Raises KeyError if either year is not in the CPI-U table.

Parameters:
  • from_year (int)

  • to_year (int)

Return type:

float

siege_utilities.geo.timeseries.longitudinal_data.validate_longitudinal_years(years, dataset='acs5')[source]

Validate and filter years for longitudinal analysis.

Parameters:
  • years (List[int]) – Requested years

  • dataset (str) – Census dataset

Returns:

List of valid years

Raises:

ValueError – If no valid years

Return type:

List[int]

Trend classification for Census time-series analysis.

This module provides functions to classify and categorize trends in longitudinal Census data. It supports:

  • Threshold-based classification (rapid growth, stable, decline, etc.)

  • Statistical classification (z-score based)

  • Custom classification schemes

Example usage:

from siege_utilities.geo.timeseries import classify_trends

# Classify income changes df = classify_trends(

df=income_data, change_column=’B19013_001E_pct_change_2010_2020’

)

# Result includes ‘trend_category’ column

class siege_utilities.geo.timeseries.trend_classifier.TrendCategory[source]

Bases: Enum

Standard trend categories for classification.

RAPID_GROWTH = 'rapid_growth'
MODERATE_GROWTH = 'moderate_growth'
STABLE = 'stable'
MODERATE_DECLINE = 'moderate_decline'
RAPID_DECLINE = 'rapid_decline'
class siege_utilities.geo.timeseries.trend_classifier.TrendThresholds[source]

Bases: object

Threshold configuration for trend classification.

rapid_growth

Threshold for rapid growth (values >= this)

Type:

float

moderate_growth

Threshold for moderate growth

Type:

float

stable_lower

Lower bound for stable category

Type:

float

moderate_decline

Threshold for moderate decline

Type:

float

rapid_decline

Values below this (or moderate_decline if None)

rapid_growth: float = 20.0
moderate_growth: float = 5.0
stable_lower: float = -5.0
moderate_decline: float = -20.0
to_dict()[source]

Convert to dictionary for display.

Return type:

Dict[str, float]

__init__(rapid_growth=20.0, moderate_growth=5.0, stable_lower=-5.0, moderate_decline=-20.0)
Parameters:
Return type:

None

siege_utilities.geo.timeseries.trend_classifier.classify_by_quantiles(df, change_column, n_quantiles=5, labels=None, output_column='trend_quantile')[source]

Classify trends into quantile-based categories.

This method divides the data into equal-sized groups (quintiles, deciles, etc.) which is useful when you want balanced category sizes.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • n_quantiles (int) – Number of quantiles (default: 5 for quintiles)

  • labels (List[str] | None) – Custom labels for quantiles. If None, uses: - 5 quantiles: [‘lowest’, ‘low’, ‘middle’, ‘high’, ‘highest’] - Other: numeric labels 1 to n

  • output_column (str) – Name for the category column

Returns:

DataFrame with quantile category column

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.trend_classifier.classify_by_zscore(df, change_column, output_column='trend_zscore_category')[source]

Classify trends using z-scores (statistical approach).

This method classifies based on how many standard deviations a geography’s change is from the mean, providing a relative classification within the dataset.

Categories: - extreme_positive: z >= 2 - high_positive: 1 <= z < 2 - average: -1 < z < 1 - high_negative: -2 < z <= -1 - extreme_negative: z <= -2

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • output_column (str) – Name for the category column

Returns:

DataFrame with z-score category column and z-score values

Return type:

pandas.DataFrame

Classify changes into trend categories.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values (typically percentage change)

  • thresholds (TrendThresholds | str | Dict[str, float] | None) – Threshold configuration. Can be: - TrendThresholds object - Preset name (‘default’, ‘conservative’, ‘sensitive’, ‘housing’, ‘population’) - Dict with keys: rapid_growth, moderate_growth, stable_lower, moderate_decline - None (uses default thresholds)

  • output_column (str) – Name for the category column

Returns:

DataFrame with new trend_category column

Return type:

pandas.DataFrame

Example

df = classify_trends(

df=income_changes, change_column=’B19013_001E_pct_change_2010_2020’, thresholds=’conservative’

)

Compare trend classifications between two DataFrames.

Useful for comparing trend patterns between different: - Time periods - Variables - Geographic subsets

Parameters:
  • df1 (pandas.DataFrame) – First DataFrame with trend classification

  • df2 (pandas.DataFrame) – Second DataFrame with trend classification

  • trend_column (str) – Name of trend category column

  • geoid_column (str) – Name of GEOID column

Returns:

DataFrame with comparison columns

Return type:

pandas.DataFrame

siege_utilities.geo.timeseries.trend_classifier.get_trend_summary(df, trend_column='trend_category', geoid_column='GEOID')[source]

Get summary statistics for trend classification.

Parameters:
  • df (pandas.DataFrame) – DataFrame with trend categories

  • trend_column (str) – Name of the trend category column

  • geoid_column (str) – Name of the GEOID column

Returns:

Dictionary with summary statistics

Return type:

Dict

siege_utilities.geo.timeseries.trend_classifier.identify_outliers(df, change_column, method='iqr', threshold=1.5)[source]

Identify outlier geographies based on change values.

Parameters:
  • df (pandas.DataFrame) – DataFrame with change metrics

  • change_column (str) – Column containing change values

  • method (str) – Outlier detection method: - ‘iqr’: Interquartile range method - ‘zscore’: Z-score method

  • threshold (float) – Threshold for outlier detection: - For IQR: multiplier (default 1.5) - For zscore: number of standard deviations (default 1.5)

Returns:

DataFrame with is_outlier and outlier_type columns

Return type:

pandas.DataFrame

Other Modules

Shared admin-level area constants for H3 and S2 resolution selection.

The average areas below are rough midpoints across the entire US. The variance within a level (e.g. NY tract vs WY tract) is huge. The resolution returned by callers is a starting point; tune up or down by +/-1 if the polygons you actually have are systematically larger or smaller.

Choropleth map creation utilities for GeoDataFrames.

Provides standalone functions that accept GeoDataFrames and return matplotlib (fig, ax) tuples. Designed for notebooks, scripts, and interactive exploration.

For ReportLab PDF integration, see reporting.chart_generator.ChartGenerator which provides parallel implementations returning ReportLab Image objects.

Functions:

create_choropleth – Single-variable choropleth create_choropleth_comparison – Multiple variables side by side create_classified_comparison – Same variable, different classification schemes create_bivariate_choropleth – Two-variable bivariate map with legend create_bivariate_crosstab – NxN count table for bivariate bins create_bivariate_companion_maps – Side-by-side monovariate maps with same bins verify_bivariate_classification – Data-level verification of classification create_bivariate_analysis – Orchestrator returning all artifacts save_map – Save figure to file with sensible defaults

class siege_utilities.geo.choropleth.BivariateAnalysisResult[source]

Bases: object

Container for all bivariate choropleth analysis artifacts.

bivariate_fig

Figure containing the bivariate choropleth map.

Type:

matplotlib.figure.Figure

bivariate_axes

Axes array [ax_map, ax_legend].

Type:

numpy.ndarray

crosstab

DataFrame of class-pair unit counts.

Type:

pandas.DataFrame

crosstab_fig

Figure of the rendered crosstab (colored grid).

Type:

matplotlib.figure.Figure

crosstab_ax

Axes for the rendered crosstab.

Type:

matplotlib.pyplot.Axes

companion_fig

Figure with two monovariate companion maps.

Type:

matplotlib.figure.Figure

companion_axes

Axes array [ax_var1, ax_var2].

Type:

numpy.ndarray

verification

Dict from verify_bivariate_classification().

Type:

Dict[str, Any]

var1_breaks

Breakpoint array for variable 1.

Type:

numpy.ndarray

var2_breaks

Breakpoint array for variable 2.

Type:

numpy.ndarray

bivariate_fig: matplotlib.figure.Figure
bivariate_axes: numpy.ndarray
crosstab: pandas.DataFrame
crosstab_fig: matplotlib.figure.Figure
crosstab_ax: matplotlib.pyplot.Axes
companion_fig: matplotlib.figure.Figure
companion_axes: numpy.ndarray
verification: Dict[str, Any]
var1_breaks: numpy.ndarray
var2_breaks: numpy.ndarray
__init__(bivariate_fig, bivariate_axes, crosstab, crosstab_fig, crosstab_ax, companion_fig, companion_axes, verification, var1_breaks, var2_breaks)
Parameters:
  • bivariate_fig (matplotlib.figure.Figure)

  • bivariate_axes (numpy.ndarray)

  • crosstab (pandas.DataFrame)

  • crosstab_fig (matplotlib.figure.Figure)

  • crosstab_ax (matplotlib.pyplot.Axes)

  • companion_fig (matplotlib.figure.Figure)

  • companion_axes (numpy.ndarray)

  • verification (Dict[str, Any])

  • var1_breaks (numpy.ndarray)

  • var2_breaks (numpy.ndarray)

Return type:

None

siege_utilities.geo.choropleth.classify_choropleth(df, column, scheme='quantiles', k=5, cmap='YlOrRd')[source]

Classify a DataFrame column and add bin/color columns.

This is the non-GDAL entry point for choropleth classification. It produces the data layer (bin assignments + hex colors) that consumers render using their platform’s native visualization.

Parameters:
  • df (pandas.DataFrame) – DataFrame (or GeoDataFrame) with a numeric column.

  • column (str) – Column name to classify.

  • scheme (str) – Classification scheme name.

  • k (int) – Number of classes.

  • cmap (str) – Matplotlib colormap name for color assignment. Requires matplotlib; omitted columns if unavailable.

Returns:

_bin: int bin assignment (0..k-1, or -1 for NaN) _break_low: lower bound of the bin _break_high: upper bound of the bin _color: hex color string (if matplotlib available)

Return type:

Copy of df with added columns

Raises:

ValueError – If column not in df or values are empty.

siege_utilities.geo.choropleth.classify_series(values, scheme='quantiles', k=5)[source]

Classify numeric values into k bins using the named scheme.

Backend dispatch: mapclassify (when installed) → pure numpy.

Parameters:
  • values (numpy.ndarray | pandas.Series | list) – Numeric array, Series, or list.

  • scheme (str) – Classification scheme name (see AVAILABLE_SCHEMES).

  • k (int) – Number of classes. Some schemes (percentiles, boxplot) ignore k.

Returns:

ClassificationResult with bin assignments, break points, and metadata.

Raises:

ValueError – If scheme is unknown or values are empty.

Return type:

ClassificationResult

siege_utilities.geo.choropleth.create_bivariate_analysis(gdf, var1, var2, *, title='Bivariate Choropleth', n_classes=3, color_scheme='purple_blue', figsize=(14, 10), edgecolor='white', linewidth=0.5, legend_size_ratio=0.25, cmap1='YlOrRd', cmap2='YlGnBu', companion_figsize=(16, 8), crosstab_figsize=(6, 5))[source]

Create a complete bivariate choropleth analysis with all verification artifacts.

One call produces: - Bivariate choropleth map with magnitude legend - Rendered crosstab showing unit counts per class pair - Two companion monovariate maps (one per variable) - Verification dict confirming classification integrity

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and two numeric columns.

  • var1 (str) – Column name for first variable (x-axis).

  • var2 (str) – Column name for second variable (y-axis).

  • title (str) – Title for the bivariate map.

  • n_classes (int) – Number of quantile classes per variable.

  • color_scheme (str | List[str]) – Bivariate color scheme name or raw list.

  • figsize (Tuple[float, float]) – Size of the bivariate map figure.

  • edgecolor (str) – Polygon edge color on the bivariate map.

  • linewidth (float) – Polygon edge width on the bivariate map.

  • legend_size_ratio (float) – Legend panel width ratio.

  • cmap1 (str) – Colormap for var1 companion map.

  • cmap2 (str) – Colormap for var2 companion map.

  • companion_figsize (Tuple[float, float]) – Size of the companion maps figure.

  • crosstab_figsize (Tuple[float, float]) – Size of the rendered crosstab figure.

Returns:

BivariateAnalysisResult dataclass with all artifacts.

Return type:

BivariateAnalysisResult

siege_utilities.geo.choropleth.create_bivariate_choropleth(gdf, var1, var2, *, title='Bivariate Choropleth', n_classes=3, color_scheme='purple_blue', figsize=(14, 10), edgecolor='white', linewidth=0.5, legend_size_ratio=0.25)[source]

Create a bivariate choropleth map showing two variables.

Bins each variable into quantile classes and combines them into a 2D color index, rendered with a proper NxN legend grid.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and two numeric columns.

  • var1 (str) – Column name for first variable (x-axis of legend).

  • var2 (str) – Column name for second variable (y-axis of legend).

  • title (str) – Map title.

  • n_classes (int) – Number of quantile classes per variable (default 3 = 9 colors).

  • color_scheme (str | List[str]) – Key into BIVARIATE_COLOR_SCHEMES or a raw list of hex colors.

  • figsize (Tuple[float, float]) – Figure size (width, height) in inches.

  • edgecolor (str) – Color of polygon edges.

  • linewidth (float) – Width of polygon edges.

  • legend_size_ratio (float) – Width ratio of the legend panel relative to the map.

Returns:

(fig, axes) tuple where axes is an array [ax_map, ax_legend].

Raises:

ValueError – If color scheme is unknown or has wrong number of colors.

Return type:

Tuple[matplotlib.figure.Figure, numpy.ndarray]

siege_utilities.geo.choropleth.create_bivariate_companion_maps(gdf, var1, var2, *, n_classes=3, cmap1='YlOrRd', cmap2='YlGnBu', figsize=(16, 8), edgecolor='black', linewidth=0.3)[source]

Create two side-by-side monovariate choropleths using the same quantile bins.

Useful as companion maps alongside a bivariate choropleth to let readers see each variable independently with the same classification.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and two numeric columns.

  • var1 (str) – Column name for first variable (left map).

  • var2 (str) – Column name for second variable (right map).

  • n_classes (int) – Number of quantile classes (same as bivariate map).

  • cmap1 (str) – Colormap for var1.

  • cmap2 (str) – Colormap for var2.

  • figsize (Tuple[float, float]) – Figure size (width, height) in inches.

  • edgecolor (str) – Edge color.

  • linewidth (float) – Edge width.

Returns:

(fig, axes) tuple where axes is a numpy array of 2 Axes.

Return type:

Tuple[matplotlib.figure.Figure, numpy.ndarray]

siege_utilities.geo.choropleth.create_bivariate_crosstab(gdf, var1, var2, *, n_classes=3, color_scheme='purple_blue', render=False, figsize=(6, 5))[source]

Create a cross-tabulation of bivariate class counts.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with two numeric columns.

  • var1 (str) – Column name for first variable (columns of crosstab).

  • var2 (str) – Column name for second variable (rows of crosstab).

  • n_classes (int) – Number of quantile classes per variable.

  • color_scheme (str | List[str]) – Key into BIVARIATE_COLOR_SCHEMES or raw list (for render colors).

  • render (bool) – If True, also return a colored matplotlib figure.

  • figsize (Tuple[float, float]) – Figure size for rendered crosstab.

Returns:

DataFrame with counts (n_classes rows × n_classes cols). If render=True: Tuple of (DataFrame, fig, ax).

Return type:

If render=False

siege_utilities.geo.choropleth.create_choropleth(gdf, column, *, title='', cmap='YlOrRd', figsize=(10, 12), legend=True, legend_kwds=None, edgecolor='black', linewidth=0.5, scheme=None, k=5, ax=None, missing_kwds=None)[source]

Create a single-variable choropleth map.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and a numeric column to visualize.

  • column (str) – Column name to color by.

  • title (str) – Map title.

  • cmap (str) – Matplotlib colormap name.

  • figsize (Tuple[float, float]) – Figure size (width, height) in inches. Ignored when ax is provided.

  • legend (bool) – Whether to show the color legend.

  • legend_kwds (Dict | None) – Dict passed to geopandas legend_kwds (e.g. label, shrink, format).

  • edgecolor (str) – Color of polygon edges.

  • linewidth (float) – Width of polygon edges.

  • scheme (str | None) – Classification scheme name (requires mapclassify). None for continuous.

  • k (int) – Number of classes when using a classification scheme.

  • ax (matplotlib.pyplot.Axes | None) – Optional matplotlib Axes to plot onto (for subplot embedding).

  • missing_kwds (Dict | None) – Dict for styling missing/NaN geometries.

Returns:

(fig, ax) tuple.

Raises:

ValueError – If scheme is unknown and no backend can handle it.

Return type:

Tuple[matplotlib.figure.Figure, matplotlib.pyplot.Axes]

siege_utilities.geo.choropleth.create_choropleth_comparison(gdf, columns, *, figsize=None, ncols=3, edgecolor='gray', linewidth=0.3)[source]

Create multiple choropleths side by side.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and data columns.

  • columns (List[Dict[str, str]]) – List of dicts, each with keys: - ‘column’ (required): Column name to visualize. - ‘title’ (optional): Subplot title. - ‘cmap’ (optional): Colormap name (default ‘YlOrRd’).

  • figsize (Tuple[float, float] | None) – Figure size. If None, auto-calculated from ncols.

  • ncols (int) – Number of columns in the subplot grid.

  • edgecolor (str) – Edge color for all subplots.

  • linewidth (float) – Edge width for all subplots.

Returns:

(fig, axes) tuple where axes is a numpy array of Axes.

Return type:

Tuple[matplotlib.figure.Figure, numpy.ndarray]

siege_utilities.geo.choropleth.create_classified_comparison(gdf, column, schemes, *, k=5, cmap='YlOrRd', figsize=None, ncols=2, edgecolor='black', linewidth=0.3, titles=None)[source]

Compare classification schemes for the same variable.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with geometry and a numeric column.

  • column (str) – Column name to classify and visualize.

  • schemes (List[str]) – List of mapclassify scheme names (e.g. ‘quantiles’, ‘fisher_jenks’).

  • k (int) – Number of classes for each scheme.

  • cmap (str) – Colormap name applied to all subplots.

  • figsize (Tuple[float, float] | None) – Figure size. If None, auto-calculated.

  • ncols (int) – Number of columns in the subplot grid.

  • edgecolor (str) – Edge color.

  • linewidth (float) – Edge width.

  • titles (List[str] | None) – Custom titles for each subplot. If None, uses SCHEME_LABELS.

Returns:

(fig, axes) tuple.

Raises:

ValueError – If a scheme is unknown and no backend can handle it.

Return type:

Tuple[matplotlib.figure.Figure, numpy.ndarray]

siege_utilities.geo.choropleth.save_map(fig, path, *, dpi=150, bbox_inches='tight', facecolor='white', transparent=False)[source]

Save a map figure to file.

Parameters:
  • fig (matplotlib.figure.Figure) – Matplotlib figure to save.

  • path (str | Path) – Output file path (PNG, PDF, SVG, etc.).

  • dpi (int) – Resolution in dots per inch.

  • bbox_inches (str) – Bounding box (‘tight’ removes whitespace).

  • facecolor (str) – Background color.

  • transparent (bool) – Whether to save with transparent background.

Returns:

Resolved Path object of the saved file.

Return type:

Path

siege_utilities.geo.choropleth.verify_bivariate_classification(gdf, var1, var2, *, n_classes=3)[source]

Verify the integrity of a bivariate quantile classification.

Pure data function — no plotting. Checks that the classification is complete, breakpoints are monotonic, and counts sum correctly.

Parameters:
  • gdf (geopandas.GeoDataFrame) – GeoDataFrame with two numeric columns.

  • var1 (str) – Column name for first variable.

  • var2 (str) – Column name for second variable.

  • n_classes (int) – Number of quantile classes per variable.

Returns:

valid (bool): True if all checks pass. total_units (int): Total number of geographic units. classified_units (int): Number successfully classified. crosstab (pd.DataFrame): Raw count crosstab. var1_breaks (np.ndarray): Breakpoint values for var1. var2_breaks (np.ndarray): Breakpoint values for var2. errors (List[str]): List of error messages (empty if valid).

Return type:

Dict with keys

Format detection and per-format attribute extraction for vector GIS files.

This module provides a small dispatcher that identifies a vector GIS file’s format (shapefile / GeoJSON / KML / KMZ / GeoPackage / DXF / SWMaps / unknown) and exposes per-format attribute extractors that tolerate missing fields the way OGR does (return None rather than raise).

Pattern lifted from a third-party tile-server audit (common/utils/DataUtils.py:488-563_getDataFromSource and the per-format _get*DataFromSource helpers). Patterns explicitly NOT carried over from the salvage source:

  • Hardcoded Windows binary paths (osgeo4WBinPath, ODA_FILE_CONVERTER_PATH) — the salvage shell-out via subprocess for DWG->DXF conversion is discarded entirely.

  • osgeo Python bindings — the salvage source uses GDAL/OGR directly; SU declares fiona (the high-level OGR wrapper) as the geo dep, so this module is fiona-shaped at the attribute-extraction layer.

  • Azure Blob fetch coupling around the dispatch.

  • Application-schema-specific extractors (map_job_id, etc.) — the generic extractors here accept candidate field-name lists; consumer code supplies the schema.

Optional dependencies are guarded; calling an extractor whose backing library is not installed raises ImportError with the install hint.

siege_utilities.geo.data_reception.detect_format(file_path)[source]

Return a format token for a vector GIS file.

Parameters:

file_path (str) – Path to a vector GIS file on disk.

Returns:

"shapefile", "geojson", "kml", "kmz", "geopackage", "dxf", "swmaps", "unknown".

Return type:

One of

Raises:

FileNotFoundError – If the file does not exist.

Detection algorithm:

  1. Extension lookup (case-insensitive) against a small canonical map.

  2. For ambiguous extensions (.json, .sqlite, .db, .zip), a magic-byte / archive-peek fallback inspects the file body.

Directories return "unknown" — walking is the caller’s responsibility.

siege_utilities.geo.data_reception.extract_attributes_kml(kml_extended_data, field_names, missing='none')[source]

Extract one attribute from a KML extended-data mapping.

Mirrors the salvage-source helper _getKMLDataFromSource (DataUtils.py:516-520). The salvage version was a one-liner dict lookup with no candidate fallback; this version adds the same candidate-list semantics as extract_attributes_ogr() for consistency across the dispatcher.

Parameters:
  • kml_extended_data (dict | None) – A flat {name: value} mapping derived from a KML <ExtendedData> block.

  • field_names (Iterable[str]) – Candidate field names, in priority order.

  • missing (str) – "none" / "raise" / "warn".

Returns:

The attribute value or None.

Return type:

Any

siege_utilities.geo.data_reception.extract_attributes_ogr(feature, field_names, missing='none')[source]

Extract one attribute from a fiona-record-shaped feature.

Mirrors the candidate-list semantics of the salvage-source helper _getOGRDataFromSource (DataUtils.py:489-513) which accepts a primary plus a fallback field name and returns None when neither is present. The salvage source used the GDAL/OGR Python bindings directly; this implementation is fiona-record-shaped (the high-level OGR wrapper that SU declares as a geo dep).

Parameters:
  • feature (Any) – A fiona-style record ({"properties": {...}, ...}) OR a plain dict (e.g. the property dict directly).

  • field_names (Iterable[str]) – Candidate field names, in priority order. First name present on the feature wins.

  • missing (str) – One of "none" (default; return None), "raise" (KeyError), or "warn" (log + return None).

Returns:

The attribute value or None.

Return type:

Any

siege_utilities.geo.data_reception.extract_attributes_swmaps(db_path, feature_id, field_names, missing='none')[source]

Extract one attribute for a SWMaps feature by attribute-field name.

Queries the SWMaps SQLite attribute_values table joined to attribute_fields for the named field, scoped to feature_id. This is the same join pattern as the salvage source’s swmapsAttributeQuery (DataUtils.py:693-698); the application-schema-specific mapper (swMapsAttributeMapper dict at DataUtils.py:672-686) is NOT lifted — that lives with the consumer.

For full SWMaps feature iteration (geometry + all attributes), use siege_utilities.geo.swmaps_reader instead; this function is the thin dispatcher-friendly accessor.

Parameters:
  • db_path (str) – Path to a SWMaps SQLite file (already extracted from .swmz if needed; archive handling lives in the swmaps_reader module).

  • feature_id (str) – SWMaps feature UUID.

  • field_names (Iterable[str]) – Candidate attribute-field names.

  • missing (str) – "none" / "raise" / "warn".

Returns:

The attribute value (as stored in attribute_values.value) or None.

Return type:

Any

Databricks-oriented fallback planning for spatial ingestion workflows.

class siege_utilities.geo.databricks_fallback.SpatialLoaderPlan[source]

Bases: object

Chosen loader with deterministic fallback ordering.

primary_loader: str
loader_order: List[str]
reason: str
__init__(primary_loader, loader_order, reason)
Parameters:
Return type:

None

siege_utilities.geo.databricks_fallback.build_census_ingest_targets(year, geography_levels, prefix='census')[source]

Build de-duplicated target table names for ingest planning.

Parameters:
Return type:

List[str]

siege_utilities.geo.databricks_fallback.build_census_table_name(year, geography_level, prefix='census')[source]

Build a normalized census table name like census_2024_block_group.

Parameters:
  • year (int)

  • geography_level (str)

  • prefix (str)

Return type:

str

siege_utilities.geo.databricks_fallback.select_spatial_loader(ogr2ogr_available, sedona_available, native_spatial_available=False)[source]

Select a spatial loader strategy for Databricks-style environments.

Loader order: 1. databricks_native 2. ogr2ogr 3. sedona 4. python

Parameters:
  • ogr2ogr_available (bool)

  • sedona_available (bool)

  • native_spatial_available (bool)

Return type:

SpatialLoaderPlan

Deprecation shim — re-exports from siege_utilities.geo.providers.nces_download.

Moved during ELE-2438. Will be removed in v4.0.0.

exception siege_utilities.geo.nces_download.NCESDownloadError[source]

Bases: Exception

Raised when an NCES download fails.

class siege_utilities.geo.nces_download.NCESDownloader[source]

Bases: object

Download NCES locale boundaries, school locations, and district data.

Downloads from the NCES EDGE (Education Demographic and Geographic Estimates) program and returns data as GeoDataFrames.

Parameters:
  • cache_dir – Directory for caching downloaded files. Defaults to a temporary directory.

  • timeout – HTTP request timeout in seconds.

Example:

downloader = NCESDownloader(cache_dir="./nces_cache")
boundaries = downloader.download_locale_boundaries(2023)
# GeoDataFrame with 12 locale territory polygons
__init__(cache_dir=None, timeout=120)[source]
Parameters:
  • cache_dir (str | None)

  • timeout (int)

download_locale_boundaries(year=2023, *, crs=None)[source]

Download NCES locale boundary polygons.

Returns a GeoDataFrame with 12 rows — one polygon per locale territory (codes 11-43). Each polygon represents the geographic extent of that locale classification.

Parameters:
Returns:

locale_code, locale_category, locale_subcategory, name, geometry.

Return type:

GeoDataFrame with columns

download_school_locations(year=2023, state_abbr=None, *, crs=None)[source]

Download geocoded school locations.

Returns a GeoDataFrame of school point locations with NCES locale codes.

Parameters:
  • year (int) – NCES publication year.

  • state_abbr (str | None) – Optional 2-letter state abbreviation to filter.

  • crs (str | None) – Output CRS. Defaults to get_default_crs().

Returns:

ncessch, school_name, lea_id, state_abbr, locale_code, locale_category, locale_subcategory, geometry.

Return type:

GeoDataFrame with columns

download_district_data(year=2023)[source]

Download district administrative data with locale codes.

Returns a DataFrame (not GeoDataFrame) of school district administrative records keyed by LEA ID.

Parameters:

year (int) – NCES publication year.

Returns:

lea_id, lea_name, state_abbr, locale_code, locale_category, locale_subcategory, survey_year.

Return type:

DataFrame with columns

RDH dataset catalog and discovery.

Pre-indexes available Redistricting Data Hub datasets by state, year, chamber, dataset type, and format for browsable discovery without trial-and-error API calls.

class siege_utilities.geo.rdh_catalog.CatalogEntry[source]

Bases: object

Indexed metadata for a single RDH dataset.

title

Human-readable dataset title.

Type:

str

url

Download URL.

Type:

str

state

Two-letter state abbreviation.

Type:

str

year

Publication or vintage year.

Type:

str

chamber

Legislative chamber (congress, state_senate, state_house, or empty).

Type:

str

dataset_type

Category (pl94171, cvap, election, legislative, etc.).

Type:

str

format

File format (csv, shp).

Type:

str

geography

Geographic level (block, tract, precinct, district, etc.).

Type:

str

official

Whether this is an official/authoritative source.

Type:

bool

file_size

Human-readable file size string.

Type:

str

title: str = ''
url: str = ''
state: str = ''
year: str = ''
chamber: str = ''
dataset_type: str = ''
format: str = ''
geography: str = ''
official: bool = False
file_size: str = ''
matches(state=None, year=None, chamber=None, dataset_type=None, format=None)[source]
Parameters:
  • state (str | None)

  • year (str | None)

  • chamber (str | None)

  • dataset_type (str | None)

  • format (str | None)

Return type:

bool

to_dict()[source]
Return type:

dict[str, Any]

classmethod from_dict(d)[source]
Parameters:

d (dict[str, Any])

Return type:

CatalogEntry

__init__(title='', url='', state='', year='', chamber='', dataset_type='', format='', geography='', official=False, file_size='')
Parameters:
Return type:

None

class siege_utilities.geo.rdh_catalog.CoverageCell[source]

Bases: object

Single cell in the coverage matrix.

state

State abbreviation.

Type:

str

year

Year.

Type:

str

dataset_type

Dataset type.

Type:

str

count

Number of datasets matching this combination.

Type:

int

formats

Set of available formats.

Type:

list[str]

state: str = ''
year: str = ''
dataset_type: str = ''
count: int = 0
formats: list[str]
__init__(state='', year='', dataset_type='', count=0, formats=<factory>)
Parameters:
Return type:

None

class siege_utilities.geo.rdh_catalog.DictRDHCatalogProvider[source]

Bases: RDHCatalogProvider

In-memory catalog provider for testing.

__init__(entries=None)[source]
Parameters:

entries (list[CatalogEntry] | None)

add(entry)[source]
Parameters:

entry (CatalogEntry)

fetch_all_datasets()[source]

Fetch the complete dataset inventory from all states.

Return type:

list[CatalogEntry]

class siege_utilities.geo.rdh_catalog.RDHCatalog[source]

Bases: object

Pre-indexed catalog of available RDH datasets.

Provides browsable discovery of datasets by state, year, chamber, dataset type, and format without trial-and-error API calls.

Parameters:
  • provider – Source for fetching the full dataset inventory.

  • cache_dir – Directory for persisting the catalog index.

  • cache_ttl – Cache time-to-live in seconds (default 30 days).

__init__(provider=None, cache_dir=None, cache_ttl=2592000)[source]
Parameters:
property entries: list[CatalogEntry]
property is_loaded: bool
property entry_count: int
load(force=False)[source]

Load the catalog, using cache if available and fresh.

Parameters:

force (bool) – If True, bypass cache and fetch from provider.

Returns:

Number of entries loaded.

Return type:

int

search(state=None, year=None, chamber=None, dataset_type=None, format=None, query=None, limit=50)[source]

Search the catalog with ranked results.

At least one filter or query must be provided.

Parameters:
  • state (str | None) – Two-letter state abbreviation.

  • year (str | None) – Publication year.

  • chamber (str | None) – Legislative chamber (congress, state_senate, state_house).

  • dataset_type (str | None) – Dataset category.

  • format (str | None) – File format (csv, shp).

  • query (str | None) – Free-text search against title.

  • limit (int) – Maximum results to return.

Returns:

List of SearchResult sorted by relevance score (descending).

Return type:

list[SearchResult]

coverage_matrix(dataset_type=None)[source]

Build a coverage matrix of state x year x dataset_type.

Parameters:

dataset_type (str | None) – Optionally filter to a single dataset type.

Returns:

List of CoverageCell entries with counts and available formats.

Return type:

list[CoverageCell]

states()[source]

Return sorted list of states with datasets in the catalog.

Return type:

list[str]

years()[source]

Return sorted list of years with datasets in the catalog.

Return type:

list[str]

dataset_types()[source]

Return sorted list of dataset types in the catalog.

Return type:

list[str]

for_state(state)[source]

Return all entries for a given state.

Parameters:

state (str)

Return type:

list[CatalogEntry]

class siege_utilities.geo.rdh_catalog.RDHCatalogProvider[source]

Bases: ABC

Protocol for fetching the full RDH dataset inventory.

abstractmethod fetch_all_datasets()[source]

Fetch the complete dataset inventory from all states.

Return type:

list[CatalogEntry]

class siege_utilities.geo.rdh_catalog.SearchResult[source]

Bases: object

Ranked search result from the catalog.

entry

The matching CatalogEntry.

Type:

siege_utilities.geo.rdh_catalog.CatalogEntry

score

Relevance score (higher = better match).

Type:

float

entry: CatalogEntry
score: float = 1.0
__init__(entry, score=1.0)
Parameters:
Return type:

None

Reader for SWMaps .swmz archives and raw .sqlite field-data files.

SWMaps is a field-data-collection tool that exports .swmz archives containing a SQLite database with three core tables:

  • features — one row per recorded feature (UUID, name, layer_id).

  • points — one row per coordinate point (fid, seq, lat, lon, elv); rows share fid and are ordered by seq.

  • feature_layers — feature-type metadata (geom_type, group_name).

The geometry rule from the salvage source: one point per feature -> POINT, two or more points -> LINESTRING.

Pattern lifted from a third-party tile-server audit (common/utils/DataUtils.py:669-783_readSWMapsSqlite). Patterns NOT carried over:

  • Hardcoded application-schema attribute mapping (map_job_id, map_group_id, map_feature_type_id, workorder_number). The consumer supplies its own attribute mapper.

  • The PG-RPC _getFeatureGroupAndTypeIdFromNames and _getJobIdFromWorkOrderNumber calls (Azure-coupled).

  • Manual WKT string concatenation — replaced with shapely.

  • _siteMapLogger macro — replaced with stdlib logging.

Typical usage:

from siege_utilities.geo.swmaps_reader import open_swmaps, read_features

with open_swmaps("/path/to/field-export.swmz") as archive:
    for feature in read_features(archive):
        print(feature["feature_type"], feature["geometry_wkt"])
class siege_utilities.geo.swmaps_reader.SWMapsArchive[source]

Bases: object

Context-manager handle to an opened SWMaps archive or raw SQLite file.

Use open_swmaps() to construct. Call close() (or use as a with block) to clean up any extracted-archive temp directory.

__init__(db_path, _temp_dir=None)[source]
Parameters:
  • db_path (str)

  • _temp_dir (str | None)

property db_path: str

Filesystem path to the readable SQLite database.

close()[source]

Remove the temp directory if this archive owned one.

Return type:

None

siege_utilities.geo.swmaps_reader.open_swmaps(path)[source]

Open a SWMaps archive (.swmz / .zip) or raw SQLite file.

Parameters:

path (str) – Path to a .swmz archive, a .zip archive containing a .sqlite member, or a raw .sqlite / .db file.

Returns:

An SWMapsArchive handle. Use it as a context manager so any extracted temp directory is cleaned up on exit.

Raises:
  • FileNotFoundError – If path does not exist.

  • ValueError – If an archive contains no SQLite member, or if the file extension is not one of the recognized forms.

Return type:

SWMapsArchive

siege_utilities.geo.swmaps_reader.read_features(archive, mapper=None)[source]

Yield per-feature dicts from a SWMaps archive.

Each yielded dict has:

  • feature_id — SWMaps UUID.

  • feature_type — from feature_layers.name.

  • feature_group — from feature_layers.group_name.

  • layer_id — from feature_layers.uuid.

  • feature_name — from features.name.

  • geometry_wkt — POINT (single coordinate) or LINESTRING (two or more coordinates), built via shapely. Z dimension included when every coordinate row carries a non-null elv; dropped otherwise.

  • attributes{field_name: value} mapping pulled from attribute_values joined to attribute_fields. Keys can be renamed by passing mapper.

Parameters:
  • archive (SWMapsArchive) – An SWMapsArchive opened via open_swmaps().

  • mapper (dict[str, str] | None) – Optional {source_name: consumer_name} rename map. When None, attribute keys are the verbatim SWMaps field names. Unknown source keys are logged and skipped (kept verbatim).

Yields:

Dicts as described above. Empty-features (zero coordinate rows) are logged at WARNING and skipped.

Raises:

ImportError – If shapely is not installed.

Return type:

Iterator[dict]

The geometry rule matches the salvage source (DataUtils.py:749-768) but uses shapely.geometry.Point / shapely.geometry.LineString with .wkt rather than manual string concatenation.