Vous savez ce qu'est une zone de chalandise. Maintenant vous voulez en tracer une, autour d'une adresse précise, et pas sur un schéma. Cet article est le mode d'emploi. Il propose deux chemins vers le même contour. Le premier passe par la démo publique, sans compte ni ligne de code, avec une réponse en quelques secondes. Le second passe par l'API, en quatre étapes, avec les appels exacts et un script Python qui écrit le contour et les sites en GeoJSON. Si la théorie vous manque (définition, zones primaire et secondaire, choix de la durée), lisez d'abord comprendre et exploiter la zone de chalandise.
Sans code : la démo
Le plus rapide est encore de regarder un contour avant d'écrire quoi que ce soit. Vous saisissez l'adresse d'un point de vente, vous choisissez 5, 10, 15, 20 ou 30 minutes, puis un mode de déplacement (voiture, vélo ou marche), et la carte affiche l'isochrone calculée sur le réseau routier réel. Ce n'est pas un cercle tracé au compas. Sous la carte, vous trouvez la liste des autres sites du réseau qui tombent à l'intérieur, chacun avec sa distance, et la fiche IRIS de l'adresse saisie. Le référentiel interrogé est fictif. Il contient mille restaurants imaginaires répartis sur la France, et il sert surtout à montrer comment des zones voisines se recouvrent.
La démo ne montre pas le nombre d'habitants à l'intérieur du contour. J'y reviens à l'étape 4, parce que c'est la question qu'on me pose le plus souvent.
Calculer une zone de chalandise dans la démo, sans compte et sans carte bancaire.
Avec l'API : quatre étapes
Les exemples qui suivent utilisent la base https://api.trustydata.app/services/v1, une clé d'API en en-tête Authorization: Bearer et la bibliothèque requests. Mettez la clé dans la variable d'environnement TRUSTYDATA_API_KEY plutôt que dans le fichier. Sinon elle finit dans le dépôt Git, et une clé qui a fuité se révoque toujours trop tard.
1. Géocoder le point de vente
Tout part d'une adresse propre. POST /address/verify la confronte au référentiel BAN officiel et renvoie les candidats classés par score, avec l'identifiant du document adresse et le code INSEE de la commune. À partir du plan Starter, la réponse ajoute la position en WGS84 et en Lambert 93.
import os
import requests
BASE = "https://api.trustydata.app/services/v1"
CLE = os.environ["TRUSTYDATA_API_KEY"]
ENTETES = {"Authorization": f"Bearer {CLE}"}
reponse = requests.post(
f"{BASE}/address/verify",
json={"q": "12 rue Scribe 75009 Paris", "max_results": 3},
headers=ENTETES,
timeout=30,
)
reponse.raise_for_status()
for candidat in reponse.json()["matches"]:
print(candidat["score"], candidat["adresse"], candidat["id"])
Regardez le score et le verdict avant d'aller plus loin. Une adresse mal saisie qui remonte un candidat moyen décale tout le contour, et personne ne s'en apercevra plus après. Gardez aussi l'id renvoyé. C'est lui qui ouvre la fiche complète de l'adresse à l'étape 4, sans repayer une recherche.
L'API traite une adresse par requête. Pour un réseau entier, la boucle se fait chez vous, dans un script ou un pipeline ETL. La dernière section de cet article montre comment.
2. Créer votre zone de chalandise par durée de trajet
Le contour et les sites qu'il contient se demandent en un seul appel à GET /locations/search, dans votre propre référentiel de lieux, celui que vous avez versé au préalable avec vos boutiques ou vos agences.
Trois paramètres définissent la zone. duree fixe le temps de trajet en minutes, de 1 à 30. mode choisit le moyen de déplacement, auto, velo ou pieton. polygone=true demande le contour lui-même, renvoyé en GeoJSON dans portee.polygone. Le centre se donne soit en lat et lon, soit en adresse libre, jamais les deux.
curl "https://api.trustydata.app/services/v1/locations/search?referentiel=mon-reseau&adresse=12+rue+Scribe+75009+Paris&duree=15&mode=auto&polygone=true" \
-H "Authorization: Bearer $TRUSTYDATA_API_KEY"
duree exclut rayon. On demande une portée en minutes ou une portée en kilomètres, pas les deux. La portée par durée (duree, mode, polygone) relève du plan Growth. La portée par rayon existe dès Starter et reste utile pour une livraison, où la contrainte est vraiment une distance.
Sur le choix de la durée, un conseil : elle dépend de l'activité, pas de l'habitude. Quinze minutes en voiture font deux ou trois kilomètres dans Paris et parfois vingt le long d'une nationale. La même valeur ne recouvre jamais la même surface deux fois. C'est bien pour cela qu'on raisonne en minutes.
3. Lire le contour et les sites dedans
La réponse tient en quatre blocs. point_central dit où la recherche a réellement porté, avec un champ precision qui vaut coordonnees, adresse ou commune. Si vous avez saisi « Lyon » au lieu d'une adresse complète, le centre est celui de la commune et le contour perd son sens. portee récapitule ce qui a sélectionné les résultats et contient le contour en GeoJSON. resultats liste les sites retenus, du plus proche au plus lointain, avec leur distance_m à vol d'oiseau. attribution porte la mention OpenStreetMap, à afficher dès qu'une isochrone est calculée.
import json
import os
import requests
BASE = "https://api.trustydata.app/services/v1"
CLE = os.environ["TRUSTYDATA_API_KEY"]
ENTETES = {"Authorization": f"Bearer {CLE}"}
reponse = requests.get(
f"{BASE}/locations/search",
params={
"referentiel": "mon-reseau",
"adresse": "12 rue Scribe 75009 Paris",
"duree": 15,
"mode": "auto",
"polygone": "true",
"limit": 200,
},
headers=ENTETES,
timeout=30,
)
reponse.raise_for_status()
data = reponse.json()
centre, portee = data["point_central"], data["portee"]
print(f"Centre : {centre['adresse_resolue']} ({centre['precision']})")
print(f"Portée : {portee['duree_min']} min en {portee['mode']}")
# Le contour, tel quel : GeoJSON relisible par QGIS, Leaflet ou GeoPandas.
with open("zone.geojson", "w", encoding="utf-8") as fichier:
json.dump(portee["polygone"], fichier, ensure_ascii=False)
# Les sites du réseau qui tombent dedans, en points.
sites = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [site["longitude"], site["latitude"]],
},
"properties": {
"id_externe": site["id_externe"],
"nom": site["nom_public"],
"distance_m": site["distance_m"],
},
}
for site in data["resultats"]
if site["latitude"] is not None and site["longitude"] is not None
],
}
with open("sites.geojson", "w", encoding="utf-8") as fichier:
json.dump(sites, fichier, ensure_ascii=False)
print(f"{len(sites['features'])} sites dans la zone")
print(data.get("attribution", ""))
Les deux fichiers s'ouvrent dans QGIS, se chargent dans GeoPandas avec geopandas.read_file() ou se passent à L.geoJSON() côté navigateur. Le contour pèse entre 10 et 20 Ko. Il tient dans une réponse HTTP, pas dans une URL, et c'est pour cela que polygone=true reste optionnel au lieu d'être renvoyé par défaut.
Un piège à connaître : le tri des résultats reste à vol d'oiseau, même quand la portée est une isochrone. Un site de l'autre côté d'un fleuve peut donc apparaître avant un site plus loin à vol d'oiseau mais plus rapide à atteindre. Si l'ordre routier compte pour vous, distances=true ajoute duree_s et distance_routiere_m aux 25 premiers résultats, et vous retriez sur ces valeurs.
4. Enrichir les adresses avec l'IRIS — par adresse
Reste à qualifier le territoire. GET /address/view/{id} reprend l'identifiant obtenu à l'étape 1, ou celui que point_central.id renvoie quand le centre a été résolu depuis une adresse, et rend la fiche complète. geocoding.code_iris et geocoding.nom_iris arrivent avec le plan Growth. Le bloc statistical_grid, le carreau INSEE Filosofi de 200 mètres, demande le plan Business.
Ce bloc enchaîne sur le précédent et réutilise requests, BASE, ENTETES et centre. Sur un plan Growth, statistical_grid est absent de la réponse plutôt que vide. D'où le .get() : un accès direct lèverait KeyError.
detail = requests.get(
f"{BASE}/address/view/{centre['id']}",
headers=ENTETES,
timeout=30,
).json()
geo = detail.get("geocoding") or {}
carreau = detail.get("statistical_grid") or {}
print(geo.get("code_iris"), geo.get("nom_iris"))
if carreau:
print(carreau["ind"], "personnes et", carreau["men"], "ménages sur le carreau")
else:
print("Carreau INSEE non disponible : passez au plan Business pour l'obtenir.")
Cet enrichissement se fait par adresse : l'API ne calcule pas la population d'une zone. C'est la limite la plus importante à connaître avant de bâtir un tableau de bord dessus, et elle change la démarche. Pour savoir qui habite dans le contour, vous géocodez votre fichier client adresse par adresse, vous récupérez le code IRIS et le carreau de chacun, puis vous comptez vous-même ceux qui tombent dans le polygone de l'étape 3. C'est un test point-dans-polygone, une ligne avec GeoPandas, ou ST_Contains avec PostGIS. J'aime mieux ce résultat qu'une estimation zonale : il porte sur vos clients réels, pas sur une population théorique.
Attention, centre["id"] vaut null quand le centre a été donné en coordonnées ou résolu au niveau d'une commune. Testez-le avant d'enchaîner.
Créer un calculateur de zone de chalandise pour votre réseau
Les quatre étapes traitent un point de vente. Pour un réseau, vous itérez. Vous listez les sites de votre référentiel, vous appelez /locations/search une fois par site avec la même durée et le même mode, et vous stockez chaque contour. Vous obtenez une couche de polygones à superposer dans QGIS ou dans un entrepôt PostGIS.
C'est là que l'exercice prend de l'intérêt. L'intersection de deux contours mesure le recouvrement entre deux magasins voisins, cette bande où le prospectus est distribué deux fois et où le client qui entre chez l'un aurait pu entrer chez l'autre. L'union des contours dessine la couverture du réseau. Son complément montre les trous, qui sont soit une occasion d'implantation, soit un territoire laissé à un concurrent. Relancez le calcul chaque trimestre et vous suivez l'effet des ouvertures et des fermetures sans refaire l'étude à la main.
Le détail du produit (référentiels de sites, capacité incluse par plan, étiquettes, horaires) est décrit sur la page zone de chalandise.
Données BAN et INSEE (sources publiques). Itinéraires © contributeurs OpenStreetMap, licence ODbL.