# Paris Solar Scorecard — Product Requirements Document

**Version:** 2.0 — FULL BUILD  
**Date:** 2026-07-08  
**Author:** Doc (OpenClaw) for Christian Petroske  
**Implementer:** Claude Code  
**Status:** READY FOR FULL IMPLEMENTATION  

---

## 1. Overview

Build the definitive solar potential map for Paris. Every building. Beautiful. Fast. Shareable. With a real action layer for copropriétés that nobody else has.

This is not a minimum viable product. This is the product. Go big.

---

## 2. The Core Experience

**Landing:** Full-screen interactive map of Paris. Every building colored by solar potential. Dark, premium, animated.

**User interactions:**
1. Pan/zoom the map — buildings colored by solar score (green/yellow/orange/red)
2. Hover any building — tooltip with address + quick score
3. Click any building — sidebar opens with full scorecard
4. Type an address in the search bar — map flies to building, sidebar opens
5. "Get AG action plan" CTA in sidebar — email capture

**The map is the product.** Not a form. Not a calculator. A map.

---

## 3. Data Pipeline (Backend)

### 3.1 Source: IGN BD Topo

**Download:** BDTOPO® from `geoservices.ign.fr`
- Format: GeoPackage (`.gpkg`)
- Layer: `BATIMENT`
- Coverage: Île-de-France tile
- Attributes required: `ID`, `HAUTEUR`, `NATURE`, geometry (polygon)

**Extract script:** `scripts/01-extract-buildings.py`
- Read GeoPackage
- Filter to Paris proper (postcodes 75001-75020)
- Output: `data/paris-buildings.geojson`

### 3.2 Compute Building Geometry

**For each building polygon:**

| Field | Method |
|---|---|
| `roof_area_m2` | `ST_Area(geometry) * 1.3` (pitched roof overshoot) |
| `lat`, `lon` | `ST_Centroid(geometry)` |
| `orientation_deg` | Azimuth of longest polygon edge |
| `typical_tilt_deg` | Rule-based on `HAUTEUR` + `NATURE` |
| `building_type` | Categorized: Haussmann / Modern / Flat / Other |

**Paris roof heuristics:**
- `HAUTEUR > 15m AND NATURE = 'Indifférenciée'` → Haussmann, 45°, zinc
- `HAUTEUR 8-15m` → Post-war, 10-15° flat/pitched
- `HAUTEUR < 8m` → Low building, varied

**Script:** `scripts/02-compute-geometry.py`

### 3.3 PVGIS Solar Irradiance

**Do NOT call PVGIS 200,000 times.** Grid sampling approach:

1. Define 250m grid over Paris bounding box
2. Query PVGIS API for each grid point (peakpower=1kWp, loss=14)
3. Cache all responses to `data/pvgis-cache/`
4. Assign each building nearest grid point's irradiance
5. Scale by actual tilt + orientation corrections

**PVGIS endpoint:** `https://re.jrc.ec.europa.eu/api/v5_2/PVcalc?lat={lat}&lon={lon}&peakpower=1&loss=14&mountingplace=building&angle=30&aspect=0&outputformat=json`

**Rate limit:** ~1 req/sec. ~1,600 grid points = ~30 min runtime.

**Script:** `scripts/03-pvgis-grid.py`

### 3.4 Final Solar Score Per Building

```
system_kwp = roof_area_m2 * 0.70 * 0.20 / 10
yearly_kwh = system_kwp * pvgis_kwh_per_kwp * tilt_correction * orientation_correction
annual_savings = yearly_kwh * 0.22
payback_years = (system_kwp * 2500) / annual_savings
co2_saved_kg = yearly_kwh * 0.06
score_per_m2 = yearly_kwh / roof_area_m2
```

**Score classification:**
| kWh/m²/year | Label | Color |
|---|---|---|
| > 180 | Excellent | #22c55e |
| 120-180 | Good | #eab308 |
| 80-120 | Fair | #f97316 |
| < 80 | Poor | #ef4444 |

**Script:** `scripts/04-compute-scores.py`
- Output: `data/paris-buildings-with-scores.geojson`

### 3.5 Generate Vector Tiles (PMTiles)

**Tool:** `tippecanoe`

```bash
tippecanoe \
  -o data/paris-solar.pmtiles \
  -z16 -Z13 \
  --drop-densest-as-needed \
  --extend-zooms-if-still-dropping \
  --force \
  -L buildings:data/paris-buildings-with-scores.geojson
```

**Single output file:** `paris-solar.pmtiles` (~50-200MB)

---

## 4. Frontend

### 4.1 Map Display

**Library:** MapLibre GL JS (free, no token)
**Base map:** CartoDB Dark Matter or MapTiler Basic (free key)
**Data layer:** PMTiles file with data-driven coloring

**Building layer styling:**
```javascript
{
  'fill-color': [
    'case',
    ['>', ['get', 'score_per_m2'], 180], '#22c55e',
    ['>', ['get', 'score_per_m2'], 120], '#eab308',
    ['>', ['get', 'score_per_m2'], 80], '#f97316',
    '#ef4444'
  ],
  'fill-opacity': 0.8,
  'fill-outline-color': '#1e293b'
}
```

**Interactions:**
- Hover: building highlights, tooltip shows `address` + `score_label`
- Click: sidebar opens with full scorecard
- Zoom: buildings fade in with opacity animation

### 4.2 Address Search

**API:** BAN (`https://api-adresse.data.gouv.fr/search/?q={query}&limit=5`)

**Flow:**
1. User types, suggestions appear
2. On selection: extract lat, lon, full label
3. Map flies to lat/lon (smooth animation, 1.5s)
4. Find nearest building within 25m
5. Open sidebar with that building's scorecard
6. Update URL: `?address=12+rue+de+Rivoli`

### 4.3 Sidebar Scorecard

**Full layout:**
- Large score badge (color-coded)
- Address (from BAN or OSM attribute)
- Metrics grid (2×3):
  - Roof area: `X m²`
  - System size: `Y kWp`
  - Yearly production: `Z kWh`
  - Annual savings: `€X,XXX`
  - Payback: `X years`
  - CO₂ saved: `X kg/year`
- Monthly production chart (12 bars, canvas-based)
- Building type tag (Haussmann / Modern / etc.)
- Context: "Enough for ~X apartments"

### 4.4 AG Action Plan CTA

**In sidebar, below scorecard:**

Headline: "Is this your building? Get the AG action plan"
Subhead: "We generate a ready-to-present document for your next general assembly"

**Form:**
- Email (required)
- Role: Copropriétaire / Syndic / Installer / Other (radio)
- Number of apartments (optional)
- Submit: "Send me the AG action plan"

**On submit:** POST to MultiplyMe backend or store in localStorage with "Coming soon" message.

### 4.5 Design System

**Colors:**
- Background: `#0f172a`
- Card: `#1e293b`
- Text primary: `#f8fafc`
- Text muted: `#94a3b8`
- Accent green: `#22c55e`
- Accent yellow: `#eab308`
- Accent orange: `#f97316`
- Accent red: `#ef4444`

**Typography:**
- System font stack
- Large numbers: 2.5rem, weight 700
- Labels: 0.875rem, weight 500, muted color

**Animations:**
- Score numbers count up on display
- Monthly bars grow upward
- Map fly-to smooth animation
- Building hover: scale 1.02, glow effect

**Mobile:**
- Sidebar becomes bottom sheet (swipe up/down)
- Search bar sticky at top
- Map full-width, full-height minus search bar

---

## 5. Technical Stack

| Component | Tool |
|---|---|
| Data processing | Python + GeoPandas |
| Tile generation | tippecanoe |
| Map rendering | MapLibre GL JS |
| Base tiles | CartoDB Dark Matter (free) |
| Address search | BAN API (client-side) |
| Hosting | Static files on Caddy |

**No backend required for core experience.** Email capture can POST to existing MultiplyMe server or be deferred.

---

## 6. File Structure

```
paris-solar/
├── data/
│   ├── ign-bdtopo/              # Downloaded IGN data
│   ├── pvgis-cache/             # API response cache
│   ├── paris-buildings.geojson  # Extracted buildings
│   ├── paris-buildings-with-scores.geojson
│   └── paris-solar.pmtiles      # Generated tiles
├── scripts/
│   ├── 01-extract-buildings.py
│   ├── 02-compute-geometry.py
│   ├── 03-pvgis-grid.py
│   └── 04-compute-scores.py
├── frontend/
│   ├── index.html
│   ├── style.css
│   ├── map.js
│   └── paris-solar.pmtiles      # Symlink or copy from data/
├── README.md
├── PRD.md
└── Makefile                     # make data, make tiles, make serve
```

---

## 7. Success Criteria

- Map loads in < 3 seconds on desktop
- Building colors render at z13-z16
- Address search → sidebar open in < 3 seconds
- Works on mobile Safari and Chrome
- PMTiles file < 200MB

---

## 8. References

- PVGIS API: https://re.jrc.ec.europa.eu/pvgis/apidoc/
- BAN API: https://api-adresse.data.gouv.fr/
- IGN data: https://geoservices.ign.fr/telechargement
- MapLibre GL JS: https://maplibre.org/
- Tippecanoe: https://github.com/felt/tippecanoe
