Le Géoportail de l'Urbanisme : une mine pour les développeurs PV
En France, toute installation photovoltaïque au sol ou en toiture dépassant une certaine puissance est soumise à la réglementation d'urbanisme locale — Plan Local d'Urbanisme (PLU), PLUi (intercommunal) ou carte communale. Une erreur de zone au stade de la prospection peut invalider des semaines de travail.
Le GPU, géré par le Ministère chargé de l'Urbanisme, centralise les documents d'urbanisme de 35 000+ communes françaises et les expose via des services WFS standards. Les données sont disponibles sans authentification ni quota.
Le GPU expose deux types de services : WMS (images raster pour l'affichage cartographique) et WFS (données vecteur pour les requêtes attributaires). Pour l'analyse programmatique (identifier la zone d'une parcelle), on utilise exclusivement le WFS.
Comprendre les zones PLU pour le photovoltaïque
Les systèmes PV sont généralement autorisés en zones :
- U (Urbaine) : Constructible, PV bâtiment autorisé, centrales au sol souvent interdites
- AU (À Urbaniser) : Constructible sous conditions – à vérifier dans le règlement
- A (Agricole) : Centrale agrivoltaïque possible, autres PV soumis à conditions strictes
- N (Naturelle) : Généralement interdit sauf exceptions PV au sol sous dérogation préfectorale
Le règlement graphique et le règlement écrit du PLU précisent les conditions exactes. L'API GPU vous donne la zone — votre équipe juridique vérifie les prescriptions textuelles.
Endpoint WFS GPU et paramètres clés
import requests
import json
from typing import Optional
# ── Endpoint principal du GPU ─────────────────────────────────────────────────
GPU_WFS_URL = "https://www.geoportail-urbanisme.gouv.fr/api/wfs/metadata"
GPU_WFS_DATA = "https://www.geoportail-urbanisme.gouv.fr/api/wfs"
# Couches disponibles (type names WFS)
COUCHES = {
"zones_urba": "GPU:zone_urba", # Zones d'urbanisme PLU/PLUi
"prescriptions": "GPU:prescription_surf", # Prescriptions surfaciques
"info_surf": "GPU:info_surf", # Informations surfaciques
"communes": "GPU:commune", # Communes avec doc d'urbanisme
}
def recuperer_zone_plu(lat: float, lon: float) -> Optional[dict]:
"""
Interroge le PLU/PLUi d'un point GPS et retourne la zone d'urbanisme.
Returns:
dict avec clés : zone_type (str), libelle (str), commune (str),
doc_id (str), constructible (bool, heuristique)
"""
params = {
"SERVICE": "WFS",
"VERSION": "2.0.0",
"REQUEST": "GetFeature",
"TYPENAMES": "GPU:zone_urba",
"CRS": "EPSG:4326",
"count": 5,
"OUTPUTFORMAT": "application/json",
# Filtre CQL : intersection avec le point GPS
"CQL_FILTER": f"INTERSECTS(geom,POINT({lon} {lat}))",
}
try:
resp = requests.get(GPU_WFS_DATA, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
except requests.RequestException as e:
return {"erreur": f"Requête GPU échouée: {e}"}
features = data.get("features", [])
if not features:
return {"zone_type": "INCONNU", "message": "Commune sans GPU numérique"}
# Prendre le premier résultat (zone la plus précise)
feat = features[0]
props = feat.get("properties", {})
libelle = props.get("libelle", "") # ex: "UA", "Ub", "AU"
typezone = props.get("typezone", "") # ex: "U", "AU", "A", "N"
liblong = props.get("libellelong", "") # ex: "Zone urbaine générale"
doc_id = props.get("partition", "") # identifiant du document
return {
"zone_type": typezone,
"libelle": libelle,
"libelle_long": liblong,
"doc_id": doc_id,
"constructible": typezone in ("U", "AU"), # heuristique simplifiée
"favorable_pv_sol": typezone == "A", # heuristique agrivoltaïque
}
Vérifier si une commune a un document GPU numérique
Toutes les communes ne sont pas encore numérisées dans GPU. Avant d'interroger les zones, vérifiez qu'un document d'urbanisme est disponible pour le code INSEE concerné.
def commune_a_gpu(code_insee: str) -> dict:
"""
Vérifie si une commune a un document d'urbanisme dans GPU et retourne
l'identifiant du document actif (PLU, PLUi, carte communale, RNU).
"""
params = {
"SERVICE": "WFS",
"VERSION": "2.0.0",
"REQUEST": "GetFeature",
"TYPENAMES": "GPU:commune",
"CQL_FILTER": f"codeinsee='{code_insee}'",
"OUTPUTFORMAT": "application/json",
}
resp = requests.get(GPU_WFS_DATA, params=params, timeout=15)
features = resp.json().get("features", [])
if not features:
return {"gpu_disponible": False, "type_doc": "RNU"}
props = features[0]["properties"]
return {
"gpu_disponible": True,
"type_doc": props.get("libelle", ""), # "PLU", "PLUi", "CC"…
"date_approb": props.get("datapprobation", ""),
"partition": props.get("partition", ""),
}
# Exemple : Lyon 7ème (INSEE 69387)
info = commune_a_gpu("69387")
print(info)
# → {'gpu_disponible': True, 'type_doc': 'PLU', 'date_approb': '2022-06-27', ...}
Pipeline de qualification automatique d'un portefeuille de sites
En prospection intensive, un bureau d'études peut avoir plusieurs centaines de sites à qualifier chaque semaine. Voici un pipeline asynchrone pour qualifier un CSV d'adresses en quelques minutes.
import asyncio
import aiohttp
import csv
async def qualifier_site_async(session, lat, lon, nom_site):
"""Version asynchrone de la qualification GPU."""
url = "https://www.geoportail-urbanisme.gouv.fr/api/wfs"
params = {
"SERVICE": "WFS", "VERSION": "2.0.0",
"REQUEST": "GetFeature", "TYPENAMES": "GPU:zone_urba",
"CQL_FILTER": f"INTERSECTS(geom,POINT({lon} {lat}))",
"OUTPUTFORMAT": "application/json", "count": 1,
}
async with session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=12)) as resp:
data = await resp.json(content_type=None)
features = data.get("features", [])
zone = features[0]["properties"]["typezone"] if features else "?"
return {"site": nom_site, "lat": lat, "lon": lon, "zone_plu": zone}
async def qualifier_portefeuille(sites: list) -> list:
"""
Qualifie tous les sites en parallèle (max 10 requêtes simultanées).
sites = [{'nom': ..., 'lat': ..., 'lon': ...}, ...]
"""
semaphore = asyncio.Semaphore(10)
async def _avec_sem(session, site):
async with semaphore:
return await qualifier_site_async(session, site["lat"], site["lon"], site["nom"])
async with aiohttp.ClientSession() as session:
tasks = [_avec_sem(session, s) for s in sites]
return await asyncio.gather(*tasks, return_exceptions=True)
Limites de l'API GPU et alternatives
- Taux de couverture national : ~80% des communes fin 2025. Les communes rurales sous Règlement National d'Urbanisme (RNU) n'ont pas de données GPU.
- Délai de mise à jour : Les documents GPU peuvent avoir 6 à 18 mois de décalage avec les délibérations municipales.
- Interprétation : L'API donne la zone, pas le règlement écrit. Une zone « AU » peut être inconstructible en l'état (AU fermée).
En cas de commune sans GPU, les alternatives sont :
- Contacter directement la mairie (DDT/DAACL)
- Consulter Géoportail IGN (fond de carte PLU rasterisé)
- Portail
api.gouv.frpour l'urbanisme (expérimental)
HeliaPV croise GPU, RPG et cadastre simultanément
Sur chaque site analysé, HeliaPV affiche automatiquement la zone PLU, la parcelle cadastrale, le classement RPG et les contraintes GeoRisques — zéro requête API à coder.
Essai gratuit – 50 analyses offertes Voir les fonctionnalités BET