Affecter un point à une zone par requête spatiale plutôt que par saisie
Sur une carte administrée par plusieurs personnes, demander à un humain dans quelle zone se trouve un point garantit qu'un jour la carte et la base ne diront plus la même chose. La géométrie sait déjà répondre.
Sur un projet de cartographie, chaque point d’intérêt devait appartenir à une zone du site : un secteur, un quai, un bâtiment. Le premier réflexe est de mettre une liste déroulante dans le formulaire d’administration et de demander à la personne qui saisit de choisir.
C’est le bon réflexe pour livrer vite, et c’est une dette qui se paie avec intérêts.
Pourquoi la saisie manuelle finit toujours par mentir
Le problème n’est pas que les gens se trompent. C’est qu’il existe alors deux sources de vérité pour le même fait : la position du point sur la carte, et la zone écrite dans la base. Rien ne les oblige à rester d’accord.
Elles divergent au premier de ces événements, et il arrivera :
- quelqu’un déplace un point sur la carte sans rouvrir la liste déroulante
- une zone est redessinée, et les points qu’elle contenait ne sont pas repassés en revue
- un import de données remplit la position mais pas la zone
- deux personnes administrent en parallèle et ne font pas la même correction
Et la divergence est silencieuse. Aucune erreur, aucune alerte : simplement une carte qui affiche un point à un endroit et un filtre qui le range ailleurs. On le découvre des mois plus tard, par un utilisateur qui ne trouve pas ce qu’il cherche.
La position contient déjà la réponse
Si les zones sont stockées comme des polygones et les points comme des points, la question « dans quelle zone est ce point ? » est une question de géométrie, pas de saisie. La base sait y répondre, à condition de lui donner le bon type de colonne.
-- Les zones, en polygones. Le SRID 4326 correspond aux coordonnées
-- longitude/latitude classiques d'un fichier GeoJSON.
CREATE TABLE zone (
id INT PRIMARY KEY AUTO_INCREMENT,
nom VARCHAR(120) NOT NULL,
contour POLYGON SRID 4326 NOT NULL,
SPATIAL INDEX (contour)
);
-- Les points d'intérêt. On ne stocke PAS de colonne zone_id :
-- ce serait recréer la deuxième source de vérité.
CREATE TABLE poi (
id INT PRIMARY KEY AUTO_INCREMENT,
nom VARCHAR(160) NOT NULL,
position POINT SRID 4326 NOT NULL,
SPATIAL INDEX (position)
);
La requête qui répond à la question :
SELECT z.id, z.nom
FROM zone z
JOIN poi p ON ST_Contains(z.contour, p.position)
WHERE p.id = ?;
ST_Contains teste si le polygone contient le point. C’est l’implémentation du classique
point-in-polygon, mais faite par le moteur, avec un index spatial derrière, et non par une
boucle dans le code applicatif.
Le piège de l’ordre des coordonnées
C’est l’erreur qui coûte le plus de temps, parce qu’elle ne produit pas d’erreur : elle produit des résultats vides.
Un fichier GeoJSON écrit ses coordonnées dans l’ordre [longitude, latitude]. MySQL, avec
le SRID 4326, attend (latitude, longitude) — l’ordre inverse. Si on recopie les coordonnées
telles quelles, les géométries sont valides, les requêtes s’exécutent, et aucun point n’est
jamais dans aucune zone. On cherche le bug dans la requête pendant des heures alors qu’il est
dans l’import.
Deux façons de s’en sortir, au choix mais une seule dans tout le projet :
-- 1. Inverser explicitement à l'import
SET @g = ST_GeomFromGeoJSON(?, 2, 4326);
-- 2. Ou demander l'ordre GeoJSON au moment de la lecture
SELECT ST_AsGeoJSON(contour, 8, 4) FROM zone;
Le test qui tranche en dix secondes : prends un point dont tu connais la zone à l’œil nu sur la carte, lance la requête. Si elle ne renvoie rien, c’est l’ordre des coordonnées, pas la logique.
Les cas limites qu’il faut décider, pas subir
Une requête spatiale répond exactement à ce qu’on lui demande. Les cas suivants ne sont pas des bugs, ce sont des décisions métier :
Un point sur la frontière exacte. ST_Contains renvoie faux pour un point situé
strictement sur le bord. Si les zones sont jointives et qu’un point tombe pile sur la limite,
il n’appartient à rien. Si c’est un problème dans ton domaine, ST_Intersects inclut la
frontière — mais alors un point sur une limite partagée appartiendra à deux zones.
Un point dans aucune zone. Un point hors du site, ou une zone qui n’a jamais été dessinée. La requête renvoie zéro ligne. Prévois l’affichage « non classé » plutôt que de laisser la carte plantée ou le filtre vide.
Des zones qui se chevauchent. La requête renvoie plusieurs lignes. Soit tu interdis le chevauchement à la saisie, soit tu décides d’une règle — la plus petite zone gagne :
SELECT z.id, z.nom
FROM zone z
JOIN poi p ON ST_Contains(z.contour, p.position)
WHERE p.id = ?
ORDER BY ST_Area(z.contour) ASC
LIMIT 1;
Le point important : chacune de ces trois situations doit être choisie. Une requête spatiale qui n’a pas répondu à ces trois questions produira un jour un résultat que personne ne saura expliquer.
Et la performance ?
L’index spatial fait le gros du travail : il élimine d’abord les polygones dont le rectangle englobant ne peut pas contenir le point, puis ne teste précisément que les rares candidats restants. Sur quelques dizaines de zones et quelques centaines de points, c’est instantané.
La vraie question de performance n’est pas la requête, c’est la fréquence. Recalculer l’affectation à chaque affichage de la carte est du gaspillage. Deux stratégies, selon le projet :
- Calculer à l’écriture : à chaque création ou déplacement d’un point, et à chaque modification d’une zone, on met à jour un champ dérivé. Rapide en lecture, mais il faut penser à recalculer toute la zone quand son contour change — c’est l’oubli classique.
- Calculer à la lecture, avec un cache : la requête reste la source de vérité, le cache est invalidé à chaque modification de zone. Plus sûr, un peu plus de code.
Dans les deux cas, la géométrie reste la référence. Le champ dérivé n’est qu’une copie jetable, et c’est précisément ce qui le distingue d’une saisie manuelle : on peut le reconstruire à tout moment par une seule requête.
Ce que j’en retiens
Chaque fois qu’on demande à un utilisateur de saisir une information que le système peut déduire, on crée une occasion de divergence. Ce n’est pas un problème de confiance envers l’utilisateur : c’est un problème de conception. Deux sources de vérité pour le même fait finissent toujours par se contredire, et c’est toujours découvert au pire moment.
La liste déroulante avait l’air d’un raccourci. C’était un emprunt.
Une question sur ce sujet, ou un problème comparable de votre côté ?Écrivez-moi.