Rechercher des utilisateurs dans un rayon géographique avec PHP et SQL
Souvent en discutant avec des associations, des clubs sportifs, des organisateurs d’évènements ou des gestionnaires de communautés, une même demande revient régulièrement : comment retrouver rapidement les adhérents, ou les clients, situés dans un rayon donné autour d’un lieu précis ? À l’heure où l’intelligence artificielle est souvent évoquée dès qu’il s’agit d’exploiter des données, ce type de besoin repose pourtant sur des mécanismes relativement simples. Une base de données correctement structurée, quelques coordonnées géographiques et quelques calculs suffisent souvent à produire des résultats pertinents en quelques millisecondes.
Nous avions déjà abordé une partie de ce type d’usage dans le chapitre Qualifier les écarts par la distance de l’article SQL en situation réelle : exploiter, corriger et fiabiliser une base existante, où nous utilisions des calculs géographiques pour enrichir l’analyse de données existantes.
Dans cet article, nous allons mettre en place une interface complète, et réutilisable, permettant de rechercher des utilisateurs autour d’un évènement ou d’un lieu donné. À partir d’un simple code postal, nous commencerons par identifier la commune concernée, gérer les cas où plusieurs communes partagent le même code postal, définir un rayon de recherche, puis interroger la base de données afin de retrouver les utilisateurs situés dans ce périmètre. Nous terminerons enfin par l’affichage et l’exportation de ces résultats. L’ensemble reposera uniquement sur PHP, MariaDB, JavaScript natif, HTML5 et CSS.
Définir le scénario avant de commencer
Avant d’écrire la moindre ligne de code, prenons quelques instants pour définir précisément ce que nous cherchons à construire. Cette étape permet de visualiser l’ensemble du parcours et d’éviter de découvrir les contraintes au fil du développement. Ce type d’approche est d’ailleurs détaillée dans l’article « Dessiner avant de coder : méthode analogique pour penser une application », qui montre comment modéliser une idée avant de passer à sa réalisation technique. Vous pouvez également consulter l’application finalisée afin de visualiser le résultat attendu avant de poursuivre.
Notre objectif est simple : définir un point de référence puis retrouver les utilisateurs situés dans un rayon donné autour de celui ci. Une fois cette sélection obtenue, les usages sont nombreux : communication ciblée, organisation d’évènements, réservations, statistiques ou encore analyses territoriales.

Pour déterminer ce point de référence, plusieurs approches sont possibles : adresse complète, coordonnées GPS, marqueur placé sur une carte ou nom de commune. Afin de rester concentrés sur le mécanisme de recherche, nous retiendrons ici une solution simple : la saisie d’un code postal.
Ce choix soulève toutefois une première difficulté. Un même code postal peut être associé à plusieurs communes. Le code postal 07310 en Ardèche en est un bon exemple, tout comme le 13100 dans les Bouches du Rhône. Une même référence postale peut donc correspondre à plusieurs localisations distinctes, donc :
- Lorsque le code postal ne correspond qu’à une seule commune, nous pourrons l’utiliser directement comme point de référence.
- En revanche, lorsqu’un même code postal est associé à plusieurs communes, il nous faudra permettre à l’utilisateur de préciser celle qui l’intéresse. Pour cela, nous utiliserons une liste de boutons radio afin de ne retenir qu’une seule localisation de départ.
Nous devrons également définir un rayon de recherche. Pour cette démonstration, nous utiliserons un curseur [range] qui nous permettra de sélectionner une distance comprise entre 0 et 300 kilomètres, valeur qui pourra naturellement être adaptée à n’importe quel contexte métier.
Une fois ces paramètres définis, nous pourrons lancer la recherche et afficher les résultats. Plusieurs modes de présentation sont envisageables, mais nous privilégierons ici un affichage tabulaire. Cette approche facilite l’exploitation des données, qu’il s’agisse d’un export CSV, d’un traitement statistique ou de l’alimentation d’un outil de diffusion de messages.

Préparer les données de travail
Avant de construire l’interface ou d’écrire la moindre requête de calcul, nous avons besoin d’un jeu de données cohérent. Sans données suffisamment riches, il devient difficile d’illustrer les mécanismes de recherche géographique que nous allons mettre en place.
Pour cet article, nous nous appuierons sur trois tables distinctes afin de montrer comment plusieurs sources d’informations peuvent être combinées au sein d’une même requête. Cette approche se retrouve dans la plupart des applications réelles, qu’il s’agisse de gérer des adhérents, des clients, des utilisateurs ou des participants à un évènement.
La première table, tab_personnas, contient les utilisateurs du système d’information : nom, prénom, adresse électronique et diverses informations de contact. Un point mérite toutefois notre attention. Le champ ch_personna_cp_id ne contient pas directement le code postal de l’utilisateur. Il référence en réalité un enregistrement de la table tab_cp, dans laquelle le code postal est stocké dans le champ ch_cp_code. Cette organisation évite de dupliquer les informations géographiques et facilite leur maintenance.

La seconde table, tab_cp, recense les communes et les codes postaux qui leur sont associés. Elle contient également les coordonnées GPS, latitude et longitude, qui serviront de base à l’ensemble de nos calculs de distance.

Enfin, la table tab_options nous permettra d’illustrer un mécanisme très courant : l’enrichissement des données par jointure. Dans notre cas, elle contient simplement les civilités associées aux utilisateurs, comme M., Mme ou Mlle, mais le principe reste identique à celui utilisé dans des structures de données beaucoup plus complexes.
Afin de nous concentrer sur la construction de l’application plutôt que sur la création des jeux de données, l’ensemble des tables et des fichiers utilisés dans cet article est mis à disposition en téléchargement.
Préparer l’environnement de développement
Pour suivre cet article, nous aurons simplement besoin d’un éditeur de code, d’un serveur PHP local et d’une base de données MariaDB ou MySQL. L’ensemble de cette configuration est déjà détaillé dans l’article Préparer son environnement de développement : VS Code et Dreamweaver côte à côte, qui regroupe les différentes étapes nécessaires pour disposer d’un environnement de travail complet.
Si certains éléments vous sont moins familiers, vous pouvez également consulter les articles Installer et configurer un serveur web en local et Démarrer avec PHPMyAdmin : création de votre première base de données, qui détaillent plus spécifiquement la mise en place du serveur et de la base de données.
Dans la suite de cet article, nous considérerons que cet environnement est déjà opérationnel afin de nous concentrer sur ce qui nous intéresse réellement : la construction progressive de notre moteur de recherche géographique et des outils qui l’accompagnent.
Le scénario étant désormais défini, nous pouvons commencer la construction de notre application en mettant en place son premier fichier PHP.
Mettre en place le premier écran
otre interface va volontairement rester très simple dans un premier temps. Avant de nous intéresser aux calculs de distance ou aux requêtes SQL, nous devons permettre à l’utilisateur de définir un point de départ. Comme évoqué précédemment, nous avons choisi de partir d’un simple code postal.
Créons donc un premier fichier home_01.html. À ce stade, une page HTML suffit largement à nos besoins. Si nous devons ultérieurement ajouter des traitements côté serveur, nous pourrons alors la faire évoluer vers une version PHP.
<h1>Recherche par distance</h1>
<label for="cp">Code postal de l'évènement</label>
<input
type="text"
id="cp_input"
name="cp_input"
maxlength="5"
value=""
inputmode="numeric"
placeholder="Entrez un CP valide"
> L’interface se limite pour le moment à un unique champ de saisie destiné à recevoir le code postal de l’évènement. Nous limitons volontairement cette saisie à cinq caractères et demandons au navigateur de privilégier un clavier numérique sur les appareils compatibles afin de faciliter la saisie.
Une fois le fichier affiché dans votre navigateur, n’hésitez pas à consulter son code source ainsi que les outils de développement accessibles avec la touche F12. Ils nous accompagneront tout au long de cet article pour observer les échanges entre l’interface, JavaScript et le serveur.

Préparer la zone de résultats et écouter la saisie
Notre champ de saisie étant en place, nous devons maintenant prévoir une zone capable d’accueillir les informations retournées par nos recherches. Dans un premier temps, elle servira simplement à afficher la ou les communes correspondant au code postal saisi. Ajoutons sous le champ de saisie un conteneur dédié :
<div id="cp_result_HTML"></div>Nous pouvons ensuite commencer à écouter les actions de l’utilisateur. Chaque modification du champ déclenchera une vérification afin de déterminer si nous disposons d’un code postal exploitable. Nous profitons également de cette étape pour supprimer les éventuels caractères non numériques et limiter la saisie à cinq chiffres.
// identifions l'espace de résultat et le champ de saisie
const cp_result_HTML = document.getElementById('cp_result_HTML');
const cpInput = document.getElementById('cp_input');
// plaçons un écouteur sur le champ de saisie
cpInput.addEventListener('input', () => {
// Expression régulière qui supprime les caractères non numériques
cpInput.value = cpInput.value.replace(/\D/g, '').slice(0, 5);
if (cpInput.value.length === 5) {
// Dès que nous avons 5 caractères, nous pouvons lancer la recherche
} else {
// sinon on vide le champ de résultat
cp_result_HTML.innerHTML = '';
}
});L’objectif est d’éviter de lancer une recherche à chaque frappe. Tant que les cinq chiffres du code postal ne sont pas disponibles, les informations restent incomplètes et ne permettent pas d’obtenir un résultat pertinent. Dès que la saisie devient exploitable, nous disposons d’un point d’entrée fiable pour interroger la base de données. Dans le chapitre suivant, nous remplacerons ce simple contrôle par une requête asynchrone vers le serveur. Pour vérifier le bon fonctionnement de cette étape intermédiaire, n’hésitez pas à consulter la version de démonstration disponible dans le fichier home_02.html.
Préparer le script serveur et son format de réponse
Une fois les cinq chiffres du code postal saisis, nous devons interroger la base de données afin de répondre à deux questions : ce code postal existe t il réellement, et est il associé à une seule commune ou à plusieurs ?
Avant de mettre en place la partie cliente, intéressons nous au script serveur chargé de fournir cette réponse. Ouvrons le fichier get_cp.php fourni avec les sources de l’article. Celui ci est intégralement commenté ; nous allons donc nous concentrer ici sur son rôle et sur le format des données retournées.
Pour simplifier les phases de test, nous utiliserons la méthode $_GET, qui permet de transmettre directement le code postal dans l’URL. Si ce mécanisme vous est moins familier, vous pouvez consulter les articles consacrés aux entrées de données en PHP ainsi qu’aux opérations asynchrones. Nous ne reviendrons pas non plus sur la connexion à la base de données avec PDO, déjà détaillée dans l’article Utiliser PDO pour gérer plusieurs types de bases de données en PHP. La requête SQL utilisée ici reste volontairement très simple : elle recherche les communes associées au code postal transmis.
$sql = "
SELECT *
FROM tab_cp
WHERE ch_cp_code = :code
ORDER BY ch_cp_label ASC
";L’élément le plus important concerne finalement le format de la réponse. Que le code postal soit invalide, associé à une seule commune ou à plusieurs, nous retournerons toujours une structure JSON homogène afin de faciliter son exploitation côté navigateur.
echo json_encode([
'success' => true, // true ou false
'count' => count($cp), // le nombre d'enregistrement retourné
'multiple' => count($cp) > 1, // true ou false, en fonction de count()
'datas' => $cp // l'objet contenant les enregistrements
]);Le champ success indique si la recherche s’est déroulée correctement. count contient le nombre d’enregistrements trouvés, multiple précise si plusieurs communes sont concernées et datas regroupe les données elles mêmes.
Avant d’écrire la moindre ligne de JavaScript, prenons le temps de vérifier le comportement du script directement dans le navigateur. Ouvrez le fichier ./php/get_cp.php

Sans paramètre, ou avec une valeur incorrecte, le script retourne une erreur. En ajoutant ?code=13490, vous obtenez les informations correspondant à Jouques. Avec ?code=07310, plusieurs communes sont retournées.

Cette étape permet de valider le fonctionnement du service indépendamment de l’interface. Nous pouvons désormais passer à la partie cliente et apprendre à exploiter cette réponse JSON depuis JavaScript.
Interroger le serveur depuis JavaScript
Nous disposons maintenant d’un script PHP capable de vérifier un code postal et de retourner les communes correspondantes au format JSON. Il ne nous reste donc plus qu’à l’invoquer depuis notre interface. Ouvrons le fichier home_03.html.
Deux points méritent d’être soulignés. Le premier concerne l’organisation du projet. Il est généralement préférable de regrouper les scripts de dialogue avec la base de données dans un dossier dédié, souvent nommé php, api ou services. Cette séparation facilite la maintenance et permet d’identifier rapidement les fichiers chargés de répondre aux requêtes du navigateur.
Le second concerne la fonction encodeURIComponent(), qui formate correctement les paramètres transmis dans une URL. Même si notre code postal ne contient ici que des chiffres, cette bonne pratique évite de nombreux problèmes lorsque les paramètres deviennent plus complexes.
// on invoque en AJAX le script get_cp.php
// notez au passage que celui ci a été volontairement placé dans un dossier PHP
fetch(`php/get_cp.php?code=${encodeURIComponent(cpInput.value)}`)
// lors de la reponse on peut la traiter
.then()
// on peut également traiter toutes eventuelles erreurs survenues au niveau du serveur
.catch(() => {
cp_result_HTML.innerHTML = 'Erreur lors de la recherche du code postal.';
});
Le fonctionnement détaillé est expliqué dans les commentaires du fichier. Retenons simplement que fetch() interroge notre script PHP sans recharger la page, puis nous permet de récupérer les données retournées par le serveur. Pour l’instant, nous nous contentons de vérifier que l’échange fonctionne correctement et d’observer les données reçues dans la console du navigateur. Une gestion minimale des erreurs est également prévue via le bloc .catch(). Dans le chapitre suivant, nous utiliserons ces données pour commencer à construire l’interface utilisateur.

Organiser les données reçues
Le serveur nous retourne désormais les informations correspondant au code postal saisi. Avant de construire le moindre affichage, prenons un instant pour réfléchir aux différents cas possibles. Un code postal peut correspondre à une seule commune ou à plusieurs communes distinctes. Nous allons donc déléguer ce traitement à une fonction dédiée : displayEventLocation().
Ouvrons le fichier . Cette fonction joue un rôle simple : analyser la réponse reçue du serveur et déterminer si nous devons afficher directement une commune ou demander à l’utilisateur de choisir parmi plusieurs possibilités.home_04.php
function displayEventLocation(arg){
if(!arg.multiple){
// traitement de la commune unique
return;
}
// traitement des communes multiples
}Cette séparation présente deux avantages. Elle rend le code plus lisible et facilite les évolutions futures en isolant clairement chaque scénario. Pour vérifier son fonctionnement, ouvrez la console du navigateur puis saisissez le code postal 13490. La console affichera « Une seule commune ». Avec 07310, elle affichera « Plusieurs communes ». Nous disposons désormais d’un point d’entrée unique pour organiser les données reçues. Nous pouvons donc passer à l’étape suivante : construire l’interface permettant de sélectionner la commune recherchée.

Construire les boutons radio de sélection
Deux scénarios sont désormais possibles. Soit le code postal correspond à une seule commune et nous pouvons la sélectionner automatiquement, soit plusieurs communes sont retournées et nous devons laisser l’utilisateur effectuer son propre choix.
Dans les deux cas, chaque bouton radio devra transporter les informations nécessaires aux traitements futurs : l’identifiant de la commune, ses coordonnées GPS ainsi que les informations d’affichage. Plutôt que de construire plusieurs fois le même bloc HTML, nous allons centraliser sa création dans une fonction dédiée, createLocationRadio(). Ouvrons le fichier .home_05.php
function createLocationRadio(data, checked = false){
return `
<label>
<input
type="radio"
name="event_location"
value="${data.ch_cp_id}"
data-lon="${data.ch_cp_lon}"
data-lat="${data.ch_cp_lat}"
${checked ? 'checked' : ''}
>
${data.ch_cp_label} (${data.ch_cp_code})
</label>
`;
}Lorsqu’une seule commune est retournée, il suffit d’invoquer cette fonction en activant le paramètre checked.
cp_result_HTML.innerHTML = `
<p>Code postal trouvé :</p>
${createLocationRadio(data, true)}
`;Lorsque plusieurs communes sont disponibles, nous parcourons simplement les enregistrements retournés par le serveur afin de générer un bouton radio pour chacune d’entre elles.
arg.datas.forEach(data => {
radios += createLocationRadio(data);
});
cp_result_HTML.innerHTML = radios;En lançant le fichier dans votre navigateur, vous verrez désormais apparaître les différentes communes correspondant au code postal saisi. Nous disposons maintenant d’un point de départ géographique exploitable. Il nous reste à mémoriser la commune sélectionnée et à préparer les critères de recherche.

Préparer la sélection de la commune et des critères de recherche
Nous disposons désormais d’un ou plusieurs boutons radio permettant d’identifier la commune associée au code postal saisi. Plutôt que d’exploiter directement les informations stockées dans ces éléments, nous allons préparer un mini formulaire chargé de centraliser les paramètres de recherche. Cette approche facilitera les échanges avec le serveur et nous permettra d’ajouter progressivement de nouveaux critères. Ouvrons le fichier home_06.html et mettons en place cette structure.
<!-- formulaire utilisé pour transmettre au serveur -->
<!-- la commune sélectionnée et le rayon de recherche -->
<form action="php/get_near.php" method="post" id="search_form">
<input type="hidden" id="event_cp_id" name="event_cp_id">
<label for="event_lat">Latitude :</label>
<input type="text" id="event_lat" name="event_lat" readonly value="">
<label for="event_lon">Longitude :</label>
<input type="text" id="event_lon" name="event_lon" readonly value="">
<button type="submit">Rechercher</button>
</form> Ce formulaire regroupe les informations qui seront transmises au serveur. Il offre également à l’utilisateur une validation visuelle du point de départ sélectionné avant le lancement de la recherche.
Dans un premier temps, nous le conserverons masqué. Il n’apparaîtra qu’une fois la commune correctement définie, qu’elle soit sélectionnée automatiquement ou choisie parmi plusieurs possibilités. Nous disposerons ainsi d’un emplacement naturel pour accueillir les paramètres complémentaires que nous ajouterons dans les prochains chapitres.
Connecter le formulaire à la commune sélectionnée
Le formulaire est désormais en place, mais aucun mécanisme ne permet encore d’y reporter les informations associées à la commune sélectionnée. Nous allons donc centraliser cette opération dans une fonction dédiée. Ouvrons le fichier home_07.html.
function selectEventLocation(radio){
document.getElementById('event_lat').value = radio.dataset.lat;
document.getElementById('event_lon').value = radio.dataset.lon;
document.getElementById('event_cp_id').value = radio.value;
document.getElementById('search_form').style.display = 'block';
}Cette fonction récupère directement les informations stockées dans les attributs du bouton radio puis les recopie dans le formulaire. Une fois les données renseignées, le formulaire peut être affiché. Il ne reste alors qu’à déclencher cette fonction lors de la sélection d’une commune. Pour cela, nous complétons la fonction createLocationRadio() en ajoutant un gestionnaire d’évènement :
onclick="selectEventLocation(this)"À chaque clic, le bouton radio transmet sa propre référence à la fonction, qui peut ainsi accéder immédiatement à ses attributs sans effectuer de recherche supplémentaire dans le document. Un dernier cas particulier doit être pris en compte. Lorsqu’un code postal ne correspond qu’à une seule commune, le bouton radio est automatiquement sélectionné lors de sa création. Aucun clic n’intervient donc pour déclencher notre fonction. Nous devons alors reproduire cette étape manuellement :
const radio = cp_result_HTML.querySelector(
'input[name="event_location"]'
);
selectEventLocation(radio);Que le code postal corresponde à une seule commune ou à plusieurs, le résultat reste identique : le formulaire reçoit toujours les informations nécessaires et reste synchronisé avec la commune actuellement sélectionnée. En lançant , vous constaterez que le formulaire apparaît désormais automatiquement dès qu’une commune est définie et que ses informations sont mises à jour à chaque changement de sélection.home_07.html
Définir le rayon de recherche
Nous pouvons désormais définir la commune de référence, mais il nous manque encore une information essentielle : jusqu’à quelle distance souhaitons nous rechercher les utilisateurs concernés ? Pour répondre à ce besoin, ouvrons le fichier et ajoutons un champ range à notre formulaire. Ce composant HTML est particulièrement adapté lorsqu’il s’agit de sélectionner une valeur comprise dans un intervalle connu.home_08.html
<label for="event_rayon">Rayon : <span id="event_rayon_label">50</span> km</label>
<input type="range" id="event_rayon" name="event_rayon" min="5" max="300" step="5" value="50">Dans notre démonstration, le rayon peut varier entre 5 et 300 kilomètres, par paliers de 5 kilomètres. Ces valeurs restent naturellement adaptables aux besoins de votre projet. Pour améliorer l’ergonomie, nous allons également actualiser en temps réel la valeur affichée à côté du curseur.
const event_rayon = document.getElementById('event_rayon');
const event_rayon_label = document.getElementById('event_rayon_label');
event_rayon.addEventListener('input', () => {
event_rayon_label.textContent = event_rayon.value;
});À chaque déplacement du curseur, le texte associé est mis à jour afin d’afficher immédiatement la distance sélectionnée. En lançant , vous pouvez désormais définir à la fois la commune de référence et le rayon de recherche. Notre formulaire contient maintenant tous les paramètres nécessaires pour préparer la recherche géographique. Nous allons pouvoir passer à la transmission de ces données vers le serveur et au calcul des distances.home_08.html
Préparer la recherche géographique côté serveur
Avant d’invoquer le serveur, préparons le script chargé de rechercher les utilisateurs situés dans le périmètre demandé. C’est probablement la partie la plus technique de cette mini application.
Comme nous l’évoquions dans l’introduction, nous avions déjà abordé une approche similaire dans le chapitre Qualifier les écarts par la distance de l’article SQL en situation réelle : exploiter, corriger et fiabiliser une base existante. Cette fois encore, nous allons nous appuyer sur une formule permettant de calculer une orthodromie, c’est à dire la distance théorique à vol d’oiseau entre deux points situés à la surface du globe.
Je vais être honnête : malgré plusieurs lectures de la documentation, ce n’est pas une formule que je trouve particulièrement intuitive. Dès que les sinus, cosinus et autres arccosinus entrent en scène, j’ai tendance à décrocher un peu. J’ai même demandé à une IA de produire une illustration afin de mieux visualiser les différents points impliqués dans le calcul. Cela aide à comprendre la logique générale, mais la formule conserve malgré tout un certain caractère abstrait.

L’essentiel n’est toutefois pas de la démontrer, mais de comprendre son rôle : transformer deux positions GPS en une distance exprimée en kilomètres.
-- Le nombre 6371 correspond au rayon moyen de la Terre exprimé en kilomètres.
-- Le reste de la formule exploite les coordonnées GPS des deux points afin de calculer la distance qui les sépare.
6371 * ACOS(
COS(RADIANS(LATITUDE POINT B))
* COS(RADIANS(LATITUDE POINT A))
* COS(RADIANS(LONGITUDE POINT A) - RADIANS(LONGITUDE POINT B))
+ SIN(RADIANS(LATITUDE POINT B))
* SIN(RADIANS(LATITUDE POINT A))
)Le nombre 6371 correspond au rayon moyen de la Terre exprimé en kilomètres. Le reste de la formule exploite les coordonnées GPS des deux points afin de calculer la distance qui les sépare.
Nous allons intégrer ce calcul dans le fichier , le service qui sera appelé par le navigateur pour rechercher les utilisateurs proches d’un évènement. Nous connaissons déjà le point de référence correspondant au lieu de l’évènement ainsi que le rayon défini par l’utilisateur. Nous allons donc demander à MariaDB de calculer la distance séparant chaque utilisateur de ce point puis de ne conserver que ceux situés dans le périmètre demandé.get_near.php
SELECT
6371 * ACOS(
COS(RADIANS(USER_CP.ch_cp_lat))
* COS(RADIANS(:event_lat))
* COS(RADIANS(:event_lon) - RADIANS(USER_CP.ch_cp_lon))
+ SIN(RADIANS(USER_CP.ch_cp_lat))
* SIN(RADIANS(:event_lat))
) AS DISTANCE_KMS
FROM tab_personnas AS USERS
JOIN tab_cp USER_CP
ON USER_CP.ch_cp_id = USERS.ch_personna_cp_id
HAVING DISTANCE_KMS < :event_rayonLa logique est finalement assez simple : une distance est calculée pour chaque utilisateur, puis la clause HAVING ne conserve que ceux dont la distance est inférieure au rayon demandé. Avant d’aller plus loin, prenons quelques minutes pour tester cette requête directement dans PHPMyAdmin. Contrairement au script précédent, nous utiliserons ici une requête POST. Il devient donc plus pratique de valider le calcul directement depuis l’onglet SQL.
En utilisant les coordonnées de Jouques (43.6334137827 ; 5.65675796328) et un rayon de 40 kilomètres, nous obtenons une série de distances correspondant aux utilisateurs situés dans ce périmètre.

Deux observations apparaissent immédiatement. Certains utilisateurs possèdent une distance égale à 0, ce qui indique qu’ils sont localisés dans la même commune que le point de référence. Par ailleurs, sur un jeu de données contenant environ 3144 utilisateurs, le calcul est exécuté en moins de 0,02 seconde, ce qui reste très confortable pour ce type d’application.
Si votre projet devait traiter plusieurs centaines de milliers d’enregistrements, il deviendrait pertinent de mettre en place une phase de présélection géographique avant d’appliquer la formule complète. Nous laisserons toutefois cette optimisation pour un futur article. Il ne reste alors qu’à compléter la requête avec les colonnes utiles, puis à améliorer légèrement la lisibilité des résultats en arrondissant la distance calculée à une décimale :
ROUND(
(
/* formule de calcul */
),
1
) AS DISTANCE_KMSEnfin, comme pour get_cp.php, nous retournons les résultats au format JSON afin qu’ils puissent être exploités directement côté navigateur.
// précisons au navigateur que ce script retourne du JSON
header('Content-Type: application/json; charset=utf-8');
// La requête SQL et son exécution PDO sont détaillées
// dans le fichier PHP get_near.php fourni avec l'article.
// fetchAll() récupère l'ensemble des lignes retournées
// par la requête et les stocke dans le tableau $users_list.
$users_list = $stmt->fetchAll();
// 'params' => $_POST, n'est pas strictement nécessaire au fonctionnement.
// on peut le transmettre surtout à des fins de vérification.
echo json_encode([
'success' => true,
'params' => $_POST,
'count' => count($users_list),
'datas' => $users_list
]);Nous disposons maintenant d’un service capable de recevoir un point GPS, un rayon de recherche et de retourner tous les utilisateurs situés dans ce périmètre. Il ne nous reste plus qu’à exploiter ces résultats côté navigateur.
Interroger le serveur et récupérer les résultats
Le script côté serveur est désormais prêt. Il ne nous reste plus qu’à lui transmettre les informations collectées depuis le formulaire. Ouvrons le fichier . La première étape consiste à placer un écouteur sur le formulaire afin d’être informé lorsqu’une recherche est déclenchée.home_09.html
document
.getElementById('search_form')
.addEventListener('submit', searchNearUsers);Nous pouvons ensuite définir la fonction searchNearUsers(), chargée de récupérer les données du formulaire puis de les transmettre au serveur. Avant toute chose, nous devons empêcher le comportement natif du navigateur. Par défaut, la soumission d’un formulaire provoque le rechargement de la page et l’envoi des données vers l’URL définie dans l’attribut action. Dans notre cas, nous souhaitons conserver l’utilisateur sur la page et communiquer avec le serveur de manière asynchrone grâce à fetch().
fetch('php/get_near.php', {
method: 'POST',
body: formData
})Comme pour notre précédent appel AJAX, la réponse retournée par le serveur est ensuite convertie en JSON puis analysée au travers d’une Promise. Pour l’instant, nous allons simplement afficher les résultats dans la console du navigateur afin de vérifier que le dialogue entre le client et le serveur fonctionne correctement.
Lancez , ouvrez les outils de développement puis effectuez une recherche. Si tout se déroule correctement, la console doit afficher la liste des utilisateurs retournés par le serveur ainsi que les informations calculées par notre requête SQL.home_09.html

Nous savons désormais sélectionner un lieu, définir un rayon, transmettre ces informations au serveur et récupérer les résultats correspondants. Il ne nous reste plus qu’à les présenter de manière exploitable pour l’utilisateur.
Afficher les utilisateurs retournés par la recherche
Nous disposons maintenant des résultats retournés par le serveur. Reste à déterminer comment les présenter à l’utilisateur. Carte interactive, liste, tableau ou représentation graphique : plusieurs approches sont possibles. Pour cet article, nous allons privilégier un affichage tabulaire. Cette solution reste simple à mettre en œuvre et prépare naturellement l’export des données que nous aborderons dans le chapitre suivant.
Commençons par réserver un emplacement destiné à recevoir les résultats. Ouvrons le fichier et ajoutons un simple conteneur HTML :home_10.html
<div id="users_result_HTML"></div>Nous allons ensuite confier l’affichage à une fonction dédiée, displayUsers(). Il suffit pour cela de remplacer l’affichage dans la console par un appel à cette fonction.
fetch(form.action, { /* paramètres */ })
.then(response => response.json())
.then(result => {
displayUsers(result);
});Cette séparation permet de distinguer clairement la communication avec le serveur de la présentation des données. Si nous souhaitions plus tard remplacer le tableau par une carte ou une autre forme d’affichage, il nous suffirait d’adapter cette fonction sans modifier le reste de l’application. La fonction displayUsers() vérifie tout d’abord que la requête s’est correctement déroulée, puis parcourt les utilisateurs retournés par le serveur afin de construire progressivement un tableau HTML. Une fois généré, celui-ci est injecté dans l’élément #users_result_HTML pour être affiché dans la page.
En lançant , les résultats apparaissent désormais directement dans l’interface. Nous pouvons modifier le code postal, ajuster le rayon de recherche puis relancer autant de fois que nécessaire. Chaque nouvelle requête remplace simplement le tableau précédent par une version actualisée.home_10.html

Exporter les résultats au format CSV
Une fois les utilisateurs identifiés, nous pouvons avoir besoin de réutiliser ces données dans d’autres outils : tableurs, statistiques, graphiques ou campagnes de communication. Le format CSV reste particulièrement adapté à ce type d’usage. Simple, léger et largement compatible, il peut être ouvert aussi bien par Excel que par LibreOffice Calc ou Google Sheets.
Comme les données ont déjà été reçues par le navigateur, il serait inutile d’interroger à nouveau le serveur. Nous allons donc les conserver dans une variable dédiée. Ouvrons le fichier et déclarons notre conteneur :home_11.html
let usersData = null;Nous alimentons ensuite cette variable dès la réception des résultats retournés par le serveur.
function searchNearUsers(event){
/* reste de la fonction */
fetch(form.action, { /* paramètres */ })
.then(response => response.json())
.then(result => {
usersData = result;
displayUsers(result); }); }Cette variable conserve le dernier jeu de données retourné par la recherche et pourra être réutilisée aussi souvent que nécessaire sans effectuer de nouvelle requête. Avant de mettre en place l’export, ajoutons un bouton dans le pied du tableau afin de rendre cette fonctionnalité accessible à l’utilisateur.
html += `
</tbody>
<tfoot>
<tr>
<th colspan="7">
<button type="button" onclick="export_csv()">Exporter ces données en CSV</button>
</th>
</tr>
</tfoot>
</table>
`;La fonction export_csv() repose alors sur un principe simple : repartir des données déjà présentes dans usersData, construire un contenu CSV puis demander au navigateur de le télécharger. L’intégralité du mécanisme est détaillée dans le fichier , notamment la création du contenu CSV, la génération d’un fichier temporaire et le déclenchement du téléchargement.home_11.html

Cette approche présente plusieurs avantages. Aucune requête supplémentaire n’est envoyée au serveur, les données déjà récupérées sont réutilisées immédiatement et le téléchargement est généré directement dans le navigateur. Le même principe pourrait d’ailleurs être adapté à d’autres formats. Rien n’empêcherait par la suite de proposer un export JSON, XML ou PDF en s’appuyant sur les mêmes données déjà présentes dans usersData.
Apporter quelques améliorations à l’interface
L’application est désormais pleinement fonctionnelle. Nous pourrions encore améliorer de nombreux aspects, mais ce n’est pas l’objectif de cet article. Jusqu’à présent, nous avons volontairement travaillé avec un HTML minimaliste afin de rester concentrés sur les mécanismes de recherche, les échanges avec PHP et les traitements réalisés par MariaDB.
Pour terminer, ouvrons le fichier et apportons quelques améliorations à l’interface. L’idée consiste simplement à mieux répartir les différentes zones de travail. La partie supérieure regroupe la saisie du code postal, tandis qu’une colonne latérale accueille la sélection de la commune et les paramètres de recherche. Les résultats occupent quant à eux la zone principale de l’écran.home_12.html

Cette organisation sépare plus clairement les critères de recherche des résultats obtenus et laisse davantage d’espace au tableau lorsque le nombre d’utilisateurs augmente. Afin de conserver une structure facile à maintenir, les styles ont été répartis dans trois feuilles distinctes :
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="layout.css">
<link rel="stylesheet" href="theme.css">Le fichier reset.css regroupe les réglages de base, layout.css gère l’organisation générale de la page et theme.css définit l’apparence visuelle de l’ensemble. Cette séparation peut sembler secondaire sur un projet de petite taille, mais elle facilite grandement les évolutions futures. Nous pouvons ainsi modifier la présentation ou l’identité graphique sans impacter les mécanismes de recherche développés tout au long de cet article.
Le fichier constitue ainsi une version plus agréable à utiliser de l’application, tout en conservant exactement les mêmes fonctionnalités que celles mises en place dans les chapitres précédents.home_12.html
Conclusion
L’application que nous venons de construire répond déjà à de nombreux besoins concrets. Avec quelques tables correctement structurées, un peu de JavaScript, quelques requêtes SQL et deux scripts PHP, nous disposons désormais d’un moteur capable d’identifier les utilisateurs situés dans un périmètre géographique donné, d’afficher les résultats puis de les exporter dans un format réutilisable.
Cette première version reste volontairement simple. Le calcul repose actuellement sur une distance à vol d’oiseau, mais rien n’empêcherait de s’appuyer demain sur une distance routière réelle « Comparer distance à vol d’oiseau et distance routière avec PHP, SQL et OpenRouteService ». De la même manière, la base de données pourrait être optimisée, les recherches enrichies par de nouveaux critères ou encore les résultats présentés sous une forme cartographique plutôt que tabulaire.
Comme souvent en développement web, le plus difficile n’est pas forcément de construire la première version d’un outil, mais d’identifier les usages qui émergeront une fois celui ci entre les mains des utilisateurs. C’est généralement à partir de ces retours du terrain que naissent les évolutions les plus pertinentes.
Cet article est né au fil de plusieurs échanges autour de problématiques de ciblage géographique rencontrées dans des projets réels. L’objectif n’était pas de proposer une solution universelle, mais de montrer que ce type de fonctionnalité reste relativement accessible. Avec quelques données correctement organisées et des outils que beaucoup de développeurs utilisent déjà au quotidien, il est possible de mettre en place rapidement une recherche géographique efficace et directement exploitable.
Le reste dépendra désormais de vos besoins, de vos contraintes et surtout des pistes que vous choisirez d’explorer.
