Setup
Install the scorer, run the CLI house simulator, and (optionally) stand up the address-search API behind the live label. Every field the simulator accepts is catalogued in the Reference.
Quick Start
git clone https://github.com/compbiolover/housing-nutrition-label.git
cd housing-nutrition-label
python3 -m venv .venv
source .venv/bin/activate
pip install -e . # installs package + dependencies
housing-simulate --preset icf-passive --lat 35.15 --lon -89.85 # score a house
Installing with pip install -e . (editable mode) gives you the housing-simulate and housing-api console commands, plus installs requests and pandas automatically from pyproject.toml.
Scoring is on-demand per address. There is no build step. The location-driven dimensions call a few keyless government APIs (FEMA flood, CDC PLACES, Census ACS) the first time a location is scored.
API Keys
Scoring any US address needs no API keys. Health, socioeconomic, and walkability are bundled national references (CDC PLACES, Census ACS, and the public-domain EPA National Walkability Index), and the geocoding and hazard/energy lookups use keyless government APIs.
Project Structure
housing-nutrition-label/
├── src/housing_label/ # Installable Python package
│ ├── config.py # Shared constants (URLs, rates, thresholds)
│ ├── utils.py # Shared helpers (HTTP, haversine, coords)
│ ├── enrich/ # Per-dimension model libraries
│ │ ├── fema_flood.py
│ │ ├── noaa_climate.py
│ │ ├── tornado.py
│ │ ├── seismic.py
│ │ ├── energy.py
│ │ ├── infrastructure.py
│ │ ├── health.py
│ │ └── socioeconomic.py
│ ├── score/ # Scoring models
│ │ ├── resilience.py # EAL-based disaster scoring
│ │ └── all_dimensions.py # Multi-dimension scorer
│ ├── data/ # Bundled offline reference lookups
│ ├── simulate/ # CLI house simulator + scoring glue
│ │ └── house.py
│ └── api.py # Address-search scoring API
├── scripts/ # build_*.py reference-data builders
├── research/ # Research & methodology docs
├── tests/ # Test directory
├── docs/ # housinglabel.dev site
├── pyproject.toml # Package metadata & dependencies
└── README.md
House Simulator
The CLI simulator lets you define a hypothetical house and see its full nutrition label (all thirteen dimensions) instantly.
Three dimensions are construction-driven (energy, durability, environmental), modeled offline from the house configuration — they are what the structure is, and do not change if you move it. Eight are location-driven (resilience, infrastructure, air_quality, noise, walkability, climate, solar, water), resolved by the house's census tract or county (no API key needed). The remaining two — health, socioeconomic — are shown as neighborhood context: full rows with their own scores and sources, but deliberately not folded into either grade, because both measure the people nearby rather than the place.
Every non-construction dimension is a keyless, bundled national reference resolved by the house's census tract or county, so scores are comparable across locations. Health (CDC PLACES), socioeconomic (Census ACS) and walkability (EPA National Walkability Index) resolve at the census tract. Air quality resolves PM2.5 and ozone at the tract (CDC Tracking, tract → county fallback) plus the county EPA radon zone, and is scored on PM2.5 + ozone alone where a county has no radon zone. Noise is a per-tract transportation-noise lookup (US DOT BTS, tract → county), refined to the parcel where no highway, arterial or railroad is near enough to carry ≥60 dB. Climate projections resolve at the tract too (CMIP6-LOCA2 sampled on a ~6 km grid, tract → county → national). Solar potential is queried at the parcel from PVGIS, falling back to a bundled per-county yield; water quality is scored for the specific system serving the address (EPA SDWIS), falling back to the county aggregate. Where the geography can't be resolved these show N/A and are excluded from the composite, never filled with a placeholder.
--no-fetch disables all network, including the Census geocoder — so no county or tract is resolved and every dimension keyed off one is left unscored. Two site dimensions survive it: disaster resilience and infrastructure burden fall back to bundled county tables and then to a national average, so they still score. That is a different question from the construction/site split above: those sets say what a score is about, not what it needs.
Full-Label Flags
# Emit the whole label (all dimensions + composite) as JSON
housing-simulate --preset icf-passive --lat 35.15 --lon -89.85 --json
# Skip live location lookups (offline; health/socio/walk left unscored)
housing-simulate --preset icf-passive --no-fetch
# Supply a location dimension directly instead of fetching it
housing-simulate --preset baseline --walk-score 79.6 --health-index 57 --socioeconomic-index 52
Using Presets
# Via the script directly
python src/housing_label/simulate/house.py --preset baseline --lat 35.15 --lon -89.85
python src/housing_label/simulate/house.py --preset icf-passive --lat 35.15 --lon -89.85
# Or via the console command (after pip install -e .)
housing-simulate --preset icf-passive --lat 35.15 --lon -89.85
# Multi-unit buildings
housing-simulate --preset duplex --lat 35.15 --lon -89.85
housing-simulate --preset icf-quadplex --lat 35.15 --lon -89.85
Available Presets
| Preset | Description |
|---|---|
baseline | 2000 wood frame, slab, average, zone X, $160k |
premium | 2026 solid brick, slab, excellent, zone X, $450k |
icf-passive | 2026 ICF, slab, excellent, zone X, $500k · solar, generator, passive house, safe room, hurricane straps, hip roof, sealed roof deck, metal roof, +1 ft elevation |
worst-case | 1945 wood frame, full basement, poor, zone AE, $80k |
fortified-gold | 2026 wood frame, slab, excellent, zone X, $350k · sealed roof deck, metal roof, FORTIFIED Gold |
duplex | 2026 solid brick, 2 units × 1,200 sqft, excellent, zone X, $300k |
quadplex | 2026 solid brick, 4 units × 900 sqft, excellent, zone X, $500k |
icf-quadplex | 2026 ICF, 4 units × 1,000 sqft, excellent, zone X, $600k · solar, passive house, hurricane straps, hip roof |
Custom Builds
# Build your own scenario
housing-simulate \
--year-built 2026 \
--construction icf \
--foundation slab \
--condition excellent \
--flood-zone X \
--lat 35.13 --lon -89.99 \
--value 330000 \
--units 2 \
--sqft 900 \
--lot-acres 0.12 \
--solar \
--hurricane-straps \
--hip-roof \
--sealed-roof-deck
Above-Code Feature Flags
| Category | Flags |
|---|---|
| Wind/Tornado | --hurricane-straps --hip-roof --impact-garage-door --sealed-roof-deck --metal-roof --reinforced-gable --ring-shank-nails |
| FORTIFIED | --fortified-roof --fortified-silver --fortified-gold |
| Seismic | --cripple-wall-bracing --seismic-hold-downs --auto-gas-shutoff --seismic-retrofit |
| Flood | --elevation-1ft --elevation-2ft --elevation-3ft --flood-vents --backflow-valve --leak-detection --sump-backup |
| General | --solar --backup-generator --passive-house --tornado-safe-room --fire-sprinklers |
| Air quality | --radon-mitigation |
Address-search API
The Examples page can score any US address with a live nutrition label. Because the site is static (GitHub Pages), the scoring runs behind a small HTTP API that wraps the CLI simulator: the same scoring path, no model drift.
Run it
pip install -e ".[api]" # installs FastAPI + uvicorn
housing-api # serves on :8000 (PORT overrides) — no API keys required
# or: uvicorn housing_label.api:app --host 0.0.0.0 --port 8000
Endpoints: GET /healthz, GET /suggest?q=<text> (address typeahead), and GET /label?address=<addr>&preset=<preset> (or ?lat=&lon=). Optional /label overrides: year_built, construction, foundation, condition, sqft, units, lot_acres, value, flood_zone, and upgrades (comma-separated resilience flags, e.g. upgrades=solar,fortified_roof,hurricane_straps). Unknown values return a 400.
Address autocomplete
GET /suggest?q=<text> returns up to five US suggestions, proxied server-side so visitors' partial text reaches only your origin (no third-party script, IP, or cookies leave the browser). With Google (Autocomplete) each result is {label, place_id, residential}. The picked one's coordinates are resolved on selection via GET /place?place_id=&session= (Place Details), which the frontend then submits to /label?lat=&lon=. With Geoapify/Photon each result already carries {label, lat, lon, residential}, scored directly. A client session token bundles a typeahead's autocomplete calls plus its one /place lookup into a single billed Google session. Free-typed addresses fall back to ?address= geocoding. The residential field flags a non-residential POI (a stadium, office, or store) so the scorer refuses to grade it as a home.
Provider (priority order):
- Google Places: set
GOOGLE_PLACES_API_KEYfor the best US coverage of business, campus, and landmark names (a company HQ that OSM-based geocoders miss). In Google Cloud: create a project, enable Places API (New), create an API key, and (recommended) restrict it to that API. Billing must be enabled; at low traffic the cost is small and Google includes a recurring free allotment (Autocomplete + Place Details are bundled into one billed session per search). The key stays server-side.GOOGLE_PLACES_AUTOCOMPLETE_URL/GOOGLE_PLACES_DETAILS_URLoverride the endpoints. - Geoapify: set
GEOAPIFY_API_KEY(free tier at geoapify.com, EU/GDPR, OSM + OpenAddresses) for sharper US ranking than Photon, no billing.GEOAPIFY_URLoverrides the endpoint. - Photon (default, keyless): OpenStreetMap.
PHOTON_URLoverridable to self-host. US business-POI coverage is limited.
Whichever is highest-priority and reachable is used; each falls back to the next if unreachable. All keys stay server-side. They never reach the browser.
Not seeing Google results? A misconfigured key falls back silently to the next provider. Hit GET /suggest?q=Unum&debug=1 on your API to diagnose (it never returns the key): configured.google: false means the key isn't in the running env. Set GOOGLE_PLACES_API_KEY and redeploy (env changes need a fresh deploy). If it's true but google_probe.ok: false, Google's message names the cause. Usually the Places API (New) isn't enabled, billing is off, or the key has an HTTP-referrer restriction (which blocks server-side calls; use None or IP restrictions instead).
Deploy it
GitHub Pages can't host the Python API. Deploy it anywhere that runs Python (Render, Fly, Cloud Run, Railway, a small VM). The repo ships two ready-to-use configs:
- Render:
render.yaml(a Blueprint). Push the repo, then New + → Blueprint and select it. The API needs no keys; optionally setGOOGLE_PLACES_API_KEY(orGEOAPIFY_API_KEY) for better address autocomplete. - Containers (Fly / Cloud Run / Railway / any host):
Dockerfile. Build withdocker build -t housing-label-api .and run with-p 8000:8000plus the env vars below.
CORS: the API restricts browser origins to https://housinglabel.dev by default. Override with the ALLOWED_ORIGINS env var (comma-separated) for a different domain or local dev, e.g. ALLOWED_ORIGINS="https://housinglabel.dev,http://localhost:8000".
Point the page at your API
The search bar reads its API base URL from, in order: a ?api= query param on the Examples page, then window.HOUSING_LABEL_API. For a permanent default, set the global before nav.js loads, e.g.:
<script>window.HOUSING_LABEL_API = "https://your-api-host"</script>
Until an API URL is configured the search bar shows a short setup note instead of calling out.
Adding a New Dimension
The scorer is designed to be extensible. To add a new scoring dimension:
- Add a model function in
src/housing_label/enrich/(for a construction-driven dimension) or a bundled lookup insrc/housing_label/data/(for a location-driven one) that returns your metric for a house / location. - Map the metric to a 0–100 score against published good-vs-poor benchmarks, and register the dimension in
src/housing_label/simulate/dimensions.py. - The national grade / percentile and the composite automatically include the new dimension.
Tech Stack
Python 3.9+ with two dependencies: requests and pandas. No geo libraries, no databases, no frameworks. All spatial calculations (Haversine, Web Mercator conversion, polygon centroids) use pure math. This is intentional: the whole scorer runs on any machine with Python installed.
The project is packaged as a standard Python package via pyproject.toml and can be installed with pip install -e . for development or built as a wheel for distribution.
Roadmap
Phase 1 (current) covers data ingestion and scoring. The longer-term vision includes:
- Durability dimension: estimated major maintenance over 15/30 years by construction type and age
- Environmental footprint: carbon, stormwater, impervious surface
- Frontend visualization: interactive nutrition label card and map explorer
- Rust scoring engine: high-performance API for real-time lookups
- API layer: REST API for Zillow/Redfin/Realtor.com integration
- Scale beyond Shelby County: parameterize for any US county