Vous savez ce qu'est une zone de chalandise. Maintenant vous voulez en créer 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. Entre les deux, un tableau de six zones réelles, chiffrées, pour choisir la durée et le mode en connaissance de cause. Si la théorie vous manque (définition, zones primaire et secondaire, isochrone ou rayon), lisez d'abord zone de chalandise : définition, méthodes de calcul et données INSEE.
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 affiche ce chiffre sous la carte : population, ménages, ménages pauvres et niveau de vie moyen de la zone ; l'étape 4 montre comment l'obtenir par l'API.
Créer une zone de chalandise dans la démo, sans compte et sans carte bancaire. Deux minutes, adresse comprise.
Quelle durée, quel mode : six zones réelles, chiffrées
Avant d'appeler quoi que ce soit, il faut choisir une durée et un mode. Le bon réflexe est de regarder ce que chaque réglage couvre réellement. Voici six zones calculées le 16 septembre 2026 par POST /zone/stats, hors heure de pointe, sur trois centres : une adresse au cœur de Lyon, le centre d'une commune de banlieue, le centre d'une commune rurale.
| Centre | Portée | Surface | Population | Ménages | Carreaux estimés |
|---|---|---|---|---|---|
| Lyon 1er, rue de la République | 10 min à pied | 1,4 km² | 21 906 | 12 073 | 2 % |
| Lyon 1er, rue de la République | 10 min à vélo | 7,9 km² | 120 376 | 63 883 | 4 % |
| Lyon 1er, rue de la République | 10 min en voiture | 66,2 km² | 546 058 | 276 289 | 14 % |
| Lyon 1er, rue de la République | 15 min en voiture | 223,7 km² | 987 041 | 469 284 | 25 % |
| Saint-Priest, centre de la commune | 10 min en voiture | 92,9 km² | 152 647 | 62 869 | 33 % |
| Marquise (Pas-de-Calais), centre de la commune | 10 min en voiture | 130,9 km² | 50 998 | 22 443 | 64 % |
Quatre choses à en tirer avant de coder.
Le mode pèse plus que la durée. Depuis la même porte lyonnaise, dix minutes à pied couvrent 22 000 habitants, dix minutes à vélo 120 000, dix minutes en voiture 546 000. Le passage de la marche au vélo multiplie la population par 5,5 ; du vélo à la voiture, par 4,5. Choisir « voiture » par défaut pour un commerce de centre-ville, c'est décrire une clientèle qui n'est pas la sienne.
Cinq minutes de plus n'ajoutent pas cinq minutes de clients. De 10 à 15 minutes en voiture, la surface est multipliée par 3,4 mais la population par 1,8 seulement : la couronne gagnée est moins dense que le cœur. C'est vrai partout, et c'est pour cela que la zone tertiaire pèse peu malgré son étendue.
La même durée ne couvre jamais la même population. Dix minutes en voiture depuis Saint-Priest couvrent 40 % de surface de plus que depuis le centre de Lyon, pour 3,6 fois moins d'habitants. Depuis Marquise, deux fois la surface de Lyon pour onze fois moins de monde. Ce sont ces écarts qui interdisent de comparer deux emplacements en kilomètres, et même en minutes sans regarder ce qu'elles contiennent.
Les carreaux estimés montent avec la ruralité. À Lyon, 14 % des carreaux Filosofi de la zone portent des valeurs imputées par l'INSEE au titre du secret statistique ; à Marquise, 64 %. Ce n'est pas un défaut de la zone, c'est la norme du fichier hors des villes denses, et l'API vous le dit dans carreaux.estimes pour que vous puissiez le dire à votre tour.
Deux centres sur trois ont été donnés par un nom de commune, et l'API l'a signalé avec point_central.precision à commune : le calcul part du centre de la commune, pas d'une porte. Pour un ordre de grandeur, c'est suffisant. Pour un emplacement, donnez l'adresse complète, l'étape 1 explique pourquoi.
Source : INSEE, Filosofi 2021 (carreaux 200 m) et Recensement de la population 2022 (IRIS) ; IGN, Contours IRIS 2026 ; itinéraires sur données OpenStreetMap, sous licence ODbL.
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": "1 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=1+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, le tableau plus haut le montre. 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": "1 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.
Ces sites sont ceux de votre référentiel « Mes lieux » ; le même référentiel sert aussi à afficher le point de vente le plus proche d'un client.
4. Chiffrer la zone, puis enrichir les adresses
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-là se fait par adresse. Pour la zone entière, c'est un autre appel, POST /zone/stats, avec le même centre, la même durée et le même mode que l'étape 3 : le contour est déjà calculé côté API, il n'est pas recalculé.
stats = requests.post(
f"{BASE}/zone/stats",
headers=ENTETES,
json={"adresse": "1 rue Scribe 75009 Paris", "duree": 15, "mode": "auto"},
timeout=30,
).json()
print(stats["population"], "habitants,", stats["menages"], "ménages,",
stats["menages_pauvres"], "sous le seuil de pauvreté")
print("niveau de vie moyen :", stats["niveau_de_vie_moyen"], "€ par personne et par an")
print(stats["iris_couverts"], "quartiers IRIS touchés,",
stats["carreaux"]["estimes"], "carreaux estimés sur", stats["carreaux"]["total"])
print(stats["attribution"])
Les chiffres sont des sommes des carreaux INSEE Filosofi de 200 mètres qui tombent dans le contour, pondérées par la part de chaque carreau dans la zone : un carreau à cheval sur la limite compte pour ce qui est dedans. Le niveau de vie est une moyenne par personne, la seule grandeur de revenu qu'un carroyage permet d'additionner. carreaux.estimes dit combien de carreaux portent des valeurs imputées par l'INSEE au titre du secret statistique ; hors des villes denses c'est souvent la majorité, et c'est la norme du fichier, pas une faiblesse de votre zone. La mention attribution se reproduit avec tout chiffre affiché. Un contour que vous avez déjà, celui de l'étape 3 ou un contour dessiné à la main, s'envoie tel quel : json={"polygone": contour}. Sources : carreaux Filosofi 200 m (INSEE, millésime 2021), Contours IRIS (IGN, édition 2026), temps de trajet sur OpenStreetMap (ODbL).
Le chiffre dit qui habite la zone, pas qui y est déjà client. C'est le complément qui rend l'exercice utile : géocodez votre fichier client adresse par adresse, récupérez le code IRIS et le carreau de chacun, comptez ceux qui tombent dans le polygone de l'étape 3 — un test point-dans-polygone, une ligne avec GeoPandas, ou ST_Contains avec PostGIS — et rapportez-les à la population rendue par /zone/stats. Vous lisez alors un taux de pénétration, la part de la population qui est cliente, et c'est lui qui départage deux emplacements.
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.
Pour aller plus loin
- Comprendre avant de créer : zone de chalandise : définition, méthodes de calcul et données INSEE, avec un cas réel à Lyon détaillé jusqu'au potentiel de dépense.
- Les termes : zone de chalandise, isochrone et zone isométrique dans le glossaire.
Données BAN et INSEE (sources publiques). Itinéraires © contributeurs OpenStreetMap, licence ODbL.