Points of Interest
Une carte de chaleur mise à jour par des signalements anonymes à proximité

Points of Interest est une preuve de concept pour cartographier une activité locale temporaire. Une personne peut signaler un point à proximité sans créer de compte. L’application vérifie la distance, enregistre le signalement et met à jour une carte de chaleur partagée en temps réel.
Je l’ai construit pour vérifier si de petits signalements anonymes peuvent montrer où une activité se concentre sans exposer de profil public.
Le problème
La plupart des cartes décrivent des lieux qui restent au même endroit. Ce projet traite une activité qui peut durer quelques minutes ou quelques heures, comme un rassemblement dans une zone.
La plupart des applications cartographiques reposent sur des lieux fixes :
Pourtant, de nombreux signaux du monde réel sont temporaires.
Un lieu peut s’animer pendant une courte période. Une zone peut devenir fréquentée. Un emplacement peut devenir pertinent parce que plusieurs personnes le signalent au même moment.
Ce type d’information n’est pas statique. Il est collectif, récent et local.
Points of Interest étudie une question simple :
Peut-on transformer des signalements anonymes à proximité en une carte utile et en temps réel de l’activité locale ?
Cette question soulève plusieurs défis techniques :
How do users submit a signal quickly?
How do we avoid requiring authentication?
How do we prevent spam or abusive submissions?
How do we validate that a signal is actually near the user?
How do we aggregate raw points into useful hotspot data?
How do we update every connected client in real time?
How do we keep the interface understandable on top of a dense map?Une simple carte de marqueurs ne suffirait pas.
Le système devait collecter, valider, stocker et agréger les signalements, diffuser les mises à jour et les restituer dans une interface géospatiale claire.
Points of Interest est ainsi passé d’une simple démonstration cartographique à un système full-stack de cartographie en temps réel.
Orientation du produit
Points of Interest est parti d’une idée simple :
Permettre à chacun de signaler anonymement une activité à proximité et d’en visualiser le résultat sur une carte de chaleur en direct.
Cette idée a rapidement pris de l’ampleur, car le produit devait trouver un équilibre entre participation, respect de la vie privée et qualité des signalements.
Pour être utile, le système devait :
-
détecter ou demander la position actuelle de l’utilisateur ;
-
permettre à l’utilisateur de signaler depuis la carte un point situé à proximité ;
-
vérifier que le point signalé est suffisamment proche de l’utilisateur ;
-
identifier les contributeurs sans compte ;
-
limiter la fréquence des contributions par client anonyme ;
-
stocker les signalements bruts avec leur horodatage ;
-
agréger les signalements récents dans des cellules de densité ;
-
exposer des instantanés au moyen d’une API ;
-
diffuser les mises à jour en direct aux clients connectés ;
-
afficher les zones à forte activité, l’activité récente et les statistiques sur les contributeurs ;
-
conserver une interface utilisable sans authentification.
L’orientation du produit était volontairement légère.
L’utilisateur ouvre l’application, autorise l’accès à sa position, touche la carte et confirme le signalement. Aucun compte ni long formulaire n’est nécessaire.
Architecture du système
Points of Interest utilise cinq parties :
React et Leaflet gèrent l’interaction avec la carte. L’API reçoit les signalements et renvoie les instantanés. Les validateurs contrôlent la fréquence et la distance. PostgreSQL stocke les signalements acceptés. Mercure envoie chaque nouvel instantané aux clients connectés.
Le système suit ce flux :
Chaque signalement accepté suit cette séquence une fois avant la réception du nouvel instantané par les clients.
Couche API backend
Le backend est une API Symfony chargée de recevoir les signalements, de renvoyer les instantanés et de coordonner les mises à jour en direct.
L’API expose une ressource centrale :
/api/signalsElle prend en charge deux opérations principales :
GET /api/signals -> return a recent signal snapshot
POST /api/signals -> submit a new signalLe contrôleur délègue l’identification du client, les instantanés et les signalements à des services séparés :
Le contrôleur traduit les requêtes HTTP en appels à ces services.
Point d’accès aux instantanés
Le point d’accès GET /api/signals renvoie un instantané de l’activité récente.
L’instantané comprend :
clientKey
points
density
latestByUser
totals
updatedAtLa réponse contient les cellules de chaleur, le nombre de contributeurs, l’activité récente et l’état du client courant. Le frontend n’agrège pas les lignes brutes de la base.
Point d’accès aux contributions
Le point d’accès POST /api/signals accepte une charge utile de signalement contenant :
signalLocation
userLocationLa position actuelle de l’utilisateur sert au contrôle de proximité. La position du signalement correspond au point indiqué sur la carte. Ces coordonnées peuvent être différentes.
Par exemple, un utilisateur peut se trouver près d’un lieu et signaler sur la carte un point légèrement en avant de sa position.
Avant de le stocker, le backend vérifie que le point signalé se trouve dans la distance autorisée.
Modèle de domaine
L’entité Signal se trouve au centre du backend.
Un signalement représente une contribution anonyme envoyée par un utilisateur.
L’entité stocke :
id
userKey
userLocation
signalLocation
createdAtLe système stocke une clé de contributeur anonyme, le point signalé, la position approximative de l’utilisateur et l’horodatage. Il ne crée ni compte, ni profil, ni mot de passe, ni identité sociale.
Le modèle de données reste ainsi léger et ciblé.
Couche de participation anonyme
Le prototype accepte les signalements sans authentification.
Les utilisateurs peuvent participer sans créer de compte.
Au lieu de stocker une identité, le backend dérive une clé client anonyme de l’adresse IP de la requête.
La clé cliente dérivée permet de gérer :
rate limiting
latest signal by contributor
contributor statistics
user-specific latest signalsans obliger les utilisateurs à s’inscrire.
Pour une preuve de concept, il s’agit d’un compromis pragmatique en matière de confidentialité.
Le système ne garantit pas un anonymat parfait. Les identifiants dérivés d’adresses IP ont des conséquences sur la vie privée, notamment si les journaux ou les adresses brutes sont conservés. Pour ce prototype, ils évitent de lier les signalements à un compte public.
Couche de validation
Les systèmes participatifs perdent toute utilité si les contributions sont trop faciles à détourner.
Points of Interest répond à ce risque avec deux protections côté backend :
rate limiting
proximity validationLimitation de la fréquence
Chaque clé client anonyme passe par le limiteur de fréquence de Symfony avant l’acceptation d’un signalement.
L’objectif est simple :
Une seule personne ne doit pas pouvoir inonder la carte de fausses activités.
Le limiteur rejette les envois répétés d’une même clé cliente pendant l’intervalle configuré.
Validation de la proximité
Le backend vérifie également que le signalement se trouve suffisamment près de la position réelle de l’utilisateur.
Le contrôle de proximité empêche un client de signaler un lieu arbitrairement éloigné.
Le processus de validation se présente ainsi :
La distance est calculée au moyen d’une fonction de type haversine.
Le backend accepte le signalement uniquement lorsque la distance calculée respecte le rayon configuré.
Couche d’instantané et d’agrégation
Les signalements bruts sont utiles pour le stockage, mais ne suffisent pas à la visualisation.
Le frontend a besoin d’une représentation de plus haut niveau :
Where are the points?
Where is activity dense?
Who submitted recently?
How many contributors are active?
When was the map last updated?Le générateur d’instantanés transforme les signalements récents en une réponse compacte.
L’agrégation de densité regroupe les signalements selon des coordonnées arrondies et incrémente un compteur d’intensité pour chaque cellule.
Elle produit ainsi des cellules prêtes pour la carte de chaleur :
lat
lng
intensityCette conception convient bien au prototype, car le frontend peut afficher la densité sans effectuer lui-même de coûteuses opérations de regroupement.
Le backend assure l’agrégation. Le frontend assure la visualisation.
Couche temps réel
Points of Interest utilise Mercure pour diffuser les mises à jour des signalements en direct.
Lorsqu’un signalement est stocké, le backend envoie un message. Un gestionnaire reconstruit l’instantané récent et le publie dans un sujet Mercure.
Les clients connectés s’abonnent au moyen de Server-Sent Events.
Ce mécanisme donne à l’application son caractère collaboratif.
Lorsqu’un utilisateur envoie un signalement, les autres utilisateurs connectés voient la carte de chaleur se mettre à jour sans actualiser la page.
C’est ce qui distingue une carte statique d’une carte participative en direct.
Couche cartographique frontend
Le frontend est une application React et Vite construite autour d’une carte Leaflet.
Son rôle consiste à transformer l’instantané du backend en une interface géospatiale interactive.
Les principaux éléments de l’interface sont :
map viewport
heatmap layer
user location marker
confirmation dialog
header overlay
sidebar panels
recent activity feed
hotspot list
status and error feedbackLe flux du frontend se présente ainsi :
Le frontend sépare les parties complexes dans différents hooks.
C’est un choix architectural solide, car le rendu de la carte, les données en direct, les statistiques dérivées et l’état de l’interface répondent à des préoccupations distinctes.
Carte de chaleur Leaflet
La couche cartographique utilise Leaflet et leaflet.heat pour représenter les cellules de densité sous forme de carte de chaleur.
La carte de chaleur reçoit les cellules de densité produites par le backend et normalise leurs valeurs d’intensité avant l’affichage.
Le client prend également en charge plusieurs fournisseurs de tuiles cartographiques, notamment OpenStreetMap et les tuiles de type Mapbox.
Le résultat est plus utile qu’un simple affichage de marqueurs individuels.
Les marqueurs montrent les événements. Les cartes de chaleur révèlent les tendances.
Cette distinction est au cœur du projet.
Conception des interactions
L’envoi d’un signalement suit ces étapes :
L’application ne demande ni nom, ni adresse e-mail, ni mot de passe, ni profil, ni catégorie.
C’est le bon choix pour ce prototype.
Le prototype demande uniquement la position nécessaire pour tester la participation anonyme.
Confirmation avant l’envoi
Un clic sur la carte n’envoie pas immédiatement un signalement.
Il ouvre d’abord une boîte de dialogue de confirmation.
La confirmation permet de vérifier la position après un clic accidentel sur la carte.
Filtrage selon un rayon local
Le frontend se concentre sur l’activité à proximité.
Il filtre les cellules de densité visibles, les points et les derniers signalements des utilisateurs autour de la position actuelle de l’utilisateur.
L’expérience reste ainsi locale, au lieu de transformer la carte en un amas mondial de points sans rapport entre eux.
Pour ce prototype, la proximité constitue le produit.
Flux de données
Le flux de données complet de l’application se présente ainsi :
Ce flux relie l’interaction sur la carte à la validation, au stockage, à l’agrégation et aux mises à jour en direct.
Il transforme des signalements individuels en une vue spatiale partagée et en direct.
Contrat de l’API
Le contrat de l’API contient une lecture d’instantané et une écriture de signalement.
Une réponse d’instantané contient :
interface Snapshot {
clientKey?: string;
points: Signal[];
density: SignalDensityCell[];
latestByUser: Signal[];
totals: {
points: number;
contributors: number;
};
updatedAt: string;
}Un signalement contient :
interface Signal {
id: string;
signalLocation: Point;
createdAt: string;
userKey: string;
}Une cellule de densité contient :
interface SignalDensityCell {
lat: number;
lng: number;
intensity: number;
}Chaque champ de réponse correspond à une carte de chaleur, un fil, un compteur ou un marqueur du client.
Le frontend n’a pas besoin de comprendre les lignes de la base de données. Il reçoit exactement les données nécessaires pour :
map points
heatmap cells
contributor count
recent activity
latest signal by user
last update timeCouche d’infrastructure
L’infrastructure locale utilise Docker Compose pour les services de soutien.
Le backend s’appuie sur :
Symfony 7
PHP 8.4
Doctrine ORM
PostgreSQL / PostGIS
Mercure
AdminerLe frontend s’appuie sur :
React
TypeScript
Vite
Leaflet
leaflet.heat
Zustand
i18next
Sonner
Tailwind-style UI componentsL’infrastructure peut être résumée ainsi :
Cette stack convient bien au projet.
Symfony valide les requêtes et distribue les messages. React affiche l’interface. Leaflet affiche la carte. Mercure diffuse les instantanés. PostgreSQL/PostGIS stocke les signalements et les coordonnées.
Qualité et intégration continue
Le dépôt comprend des contrôles qualité pour le backend comme pour le frontend.
Le processus qualité du backend exécute :
PHPStan
ECS coding standards
PHPUnitLe processus qualité du frontend exécute :
ESLint
Prettier
TypeScript type checkingLe dépôt couvre une API, une base de données, un transport de messages et un client cartographique.
Le projet est suffisamment structuré pour être maintenu, testé et étendu.
Les modifications du backend et du frontend déclenchent des contrôles séparés.
Les modifications du serveur déclenchent les processus PHP. Celles du client déclenchent les processus Node. L’intégration continue reste ainsi ciblée et moins coûteuse à exécuter.
Principales décisions d’ingénierie
Construire le produit autour d’instantanés en direct
Le projet ne diffuse pas au frontend les événements individuels de bas niveau issus de la base de données.
Il diffuse plutôt des instantanés actualisés.
Ce choix convient au prototype, car l’interface a besoin de l’état actuel de la carte plutôt que d’un journal complet des événements.
Un instantané fournit au frontend une réalité simple :
Here are the current points.
Here is the current density.
Here are the current contributors.
Here is the latest update time.Préserver l’anonymat des utilisateurs
L’application évite entièrement l’authentification.
Cela réduit les frictions et facilite l’utilisation du prototype.
La clé client anonyme fournit néanmoins au système une structure suffisante pour limiter la fréquence des contributions et calculer les statistiques des contributeurs, sans introduire de gestion des comptes.
C’est le bon compromis pour une expérimentation participative autour de signalements locaux.
Valider la proximité côté backend
Le frontend peut guider l’utilisateur, mais le backend doit faire respecter la règle.
Points of Interest vérifie sur le serveur la distance entre la position de l’utilisateur et celle du signalement.
Le client ne devient donc pas la source de vérité.
Il ne faut jamais faire confiance au navigateur pour ce type de validation. Ce serait une erreur.
Séparer l’envoi de la visualisation
L’envoi et la visualisation suivent des flux distincts.
Un utilisateur envoie un signalement. Le système reconstruit l’instantané. Chaque client reçoit la vue mise à jour.
Le modèle mental reste ainsi simple :
Utiliser des cartes de chaleur, pas seulement des marqueurs
Les marqueurs individuels sont utiles, mais représentent mal la densité.
Les cartes de chaleur révèlent les tendances.
Dans un système participatif de zones à forte activité, la densité constitue le produit. La carte de chaleur n’est pas décorative : elle est l’interface principale.
Utiliser des hooks frontend dédiés
Le code React sépare les responsabilités au moyen de hooks :
useUserLocation
useHotspotFeed
useLeafletHeatmap
useFeedDerivations
useAppStoreChaque hook gère un état précis, comme la géolocalisation, Mercure, les données dérivées du fil ou l’instance Leaflet.
Le cycle de vie de la carte, les abonnements Mercure, les listes dérivées de zones à forte activité, la géolocalisation et l’état de l’interface ne devraient pas tous résider dans un unique composant volumineux.
Utiliser Mercure pour les mises à jour en temps réel
Mercure convient bien à ce cas d’usage, car le frontend n’a besoin que de mises à jour du serveur vers le client.
Le client n’a pas besoin d’un protocole WebSocket bidirectionnel complet. Les Server-Sent Events suffisent.
Le client ouvre uniquement un flux d’événements du serveur vers le client.
Maintenir un contrat d’API adapté à l’interface
L’API renvoie directement les cellules de densité, les derniers signalements par utilisateur, les totaux et les horodatages de mise à jour.
Le frontend affiche ces champs préparés sans recalculer la densité.
Le backend prépare les données. Le frontend les présente.
Ce que j’ai appris
La principale leçon de Points of Interest est que la carte n’est que la dernière étape. L’application doit d’abord rejeter les envois trop éloignés ou répétés, regrouper les coordonnées en cellules de densité, diffuser les mises à jour et garder l’interface lisible.
J’ai également appris que l’anonymat modifie l’architecture.
Sans comptes, le système doit tout de même distinguer les contributeurs, appliquer des limites et afficher "mon dernier signalement". Une clé client anonyme suffit pour ce prototype. Elle ne garantit pas un anonymat fort et demanderait un modèle de menace plus précis avant tout usage avec des signalements sensibles.
Le projet a aussi montré que les points bruts ne suffisent pas. Le service d’instantanés les transforme en cellules de densité, nombres de contributeurs et activité récente que l’interface peut afficher sans reprendre la logique d’agrégation.
État actuel
La preuve de concept couvre les clés clientes anonymes, le contrôle de proximité, la limitation de fréquence, la persistance, les instantanés de densité, les mises à jour Mercure et le rendu de la carte de chaleur. Elle démontre le parcours complet, mais ne prouve pas encore que les utilisateurs enverront assez de signalements exacts pour rendre la carte utile.
Le prochain test doit porter sur ce risque produit, pas sur un nouveau framework. Je recruterais un petit groupe dans une zone, définirais un cas d’usage et mesurerais l’exactitude, la participation, la latence et les abus.