Une carte de recherche de prestataires de santé est l'une des cartes les plus importantes qu'un développeur puisse construire. Les patients qui l'utilisent sont souvent anxieux, douloureux ou naviguent dans un système inconnu. Une carte qui se charge lentement, retourne des résultats non pertinents ou envoie silencieusement leur code postal à un service tiers basé aux États-Unis n'est pas seulement une mauvaise expérience utilisateur, c'est une violation potentielle du RGPD.
Ce didacticiel construit une recherche de prestataire prête pour la production : filtre de spécialité, recherche par code postal en utilisant l'API de géocodage, superposition isochrone montrant « médecins dans 20 minutes » et une carte fonctionnelle en utilisant l'API de cartes MapAtlas, tout avec la conformité RGPD intégrée à l'architecture dès le départ, pas rétrofittée après.
L'implémentation complète est du JavaScript vanille, aucun cadre requis. Cela fonctionne sur n'importe quel stack : React, Vue, HTML simple, WordPress ou un CMS hérité d'un hôpital. Le didacticiel couvre également le contexte réglementaire européen, les données CQC du Royaume-Uni, la Kassenärztliche Vereinigung allemande et la CNAM française, pour que vous sachiez où trouver légitimement les données du prestataire.
Pourquoi les applications de localisation de santé ont des obligations RGPD supplémentaires
Les applications standard collectent des données personnelles (nom, email, utilisation). Les applications de santé ont un deuxième risque : inférer des informations de santé à partir des données de localisation.
Quand un utilisateur recherche « cardiologues près de SE1 7PB », son code postal est une donnée personnelle. La combinaison du code postal et de la spécialité médicale devient potentiellement une donnée de catégorie spéciale en vertu de l'article 9(1) du RGPD, une donnée révélant une condition de santé. Cela est vrai même si vous ne créez jamais de compte utilisateur ou ne stockez de profil.
Les risques sont concrets :
- Un appel d'API de géocodage côté client qui inclut le code postal tapé par l'utilisateur est envoyé à un serveur tiers. Ce serveur voit le code postal, la spécialité recherchée et l'adresse IP. Sans un DPA et une garantie de résidence des données dans l'UE, c'est un transfert problématique.
- L'autocomplétion et l'historique du navigateur peuvent persister le code postal tapé et la requête de spécialiste sur l'appareil de l'utilisateur, en dehors de votre contrôle mais méritant d'être noté dans vos informations de confidentialité.
- Si vous enregistrez les réponses API côté serveur (courant pour le débogage), vous enregistrez peut-être des combinaisons de code postal + spécialité sans le réaliser.
La correction est le proxying backend : votre serveur reçoit la recherche, appelle l'API de géocodage MapAtlas et retourne uniquement les coordonnées, n'envoyant jamais la requête tapée à une API tiers depuis le navigateur du patient. Plus sur cela dans la section mise en place.
Structure des données du prestataire
Les données du prestataire doivent provenir de sources officielles faisant autorité, pas de données grattées :
- Royaume-Uni : l'API NHS Choices de NHS Digital et les listes de prestataires enregistrés par l'CQC
- Allemagne : les données ouvertes de Kassenärztliche Bundesvereinigung (KBV) et l'API Arztsuche
- France : l'API Ameli de la CNAM et le répertoire RPPS
Structurez chaque prestataire comme une entité GeoJSON :
const providers = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9051, 52.3680] },
properties: {
id: "prov-001",
name: "Dr. Maria van den Berg",
specialty: "general-practitioner",
accepting: true,
address: "Prinsengracht 263, 1016 GV Amsterdam",
phone: "+31 20 423 5678",
languages: ["Dutch", "English"],
nextAvailable: "2026-02-18"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8890, 52.3610] },
properties: {
id: "prov-002",
name: "Dr. Jan Smits",
specialty: "cardiologist",
accepting: false,
address: "Leidseplein 15, 1017 PS Amsterdam",
phone: "+31 20 612 3456",
languages: ["Dutch"],
nextAvailable: null
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9200, 52.3750] },
properties: {
id: "prov-003",
name: "Anna Fischer, MD",
specialty: "dermatologist",
accepting: true,
address: "Plantage Middenlaan 14, 1018 DD Amsterdam",
phone: "+31 20 789 0123",
languages: ["Dutch", "English", "German"],
nextAvailable: "2026-02-20"
}
}
]
};
Étape 1 : Initialisation de la carte avec filtre de spécialité
Commencez par la carte et une liste déroulante de spécialité. Le filtre met à jour les marqueurs visibles côté client sans aucune requête réseau.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'provider-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
map.on('load', () => {
map.addSource('providers', {
type: 'geojson',
data: providers
});
// Provider markers, green = accepting, grey = full
map.addLayer({
id: 'provider-markers',
type: 'circle',
source: 'providers',
paint: {
'circle-radius': 10,
'circle-color': [
'case',
['==', ['get', 'accepting'], true], '#16A34A',
'#9CA3AF'
],
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
buildList(providers.features);
});
// Specialty filter
document.getElementById('specialty-select').addEventListener('change', (e) => {
const specialty = e.target.value;
if (specialty === 'all') {
map.setFilter('provider-markers', null);
} else {
map.setFilter('provider-markers', ['==', ['get', 'specialty'], specialty]);
}
});
Étape 2 : Recherche par code postal via mandataire backend
Au lieu d'appeler l'API de géocodage depuis le navigateur (qui expose le code postal du patient à un serveur tiers), acheminez la requête de géocodage par votre propre point d'extrémité backend. Votre serveur appelle MapAtlas et retourne uniquement les coordonnées.
Frontend (navigateur du patient) :
async function geocodePostcode(postcode) {
// Call YOUR backend, not the geocoding API directly
const res = await fetch(`/api/geocode?q=${encodeURIComponent(postcode)}`);
const { lat, lng } = await res.json();
return { lat, lng };
}
document.getElementById('postcode-form').addEventListener('submit', async (e) => {
e.preventDefault();
const postcode = document.getElementById('postcode-input').value.trim();
const { lat, lng } = await geocodePostcode(postcode);
map.flyTo({ center: [lng, lat], zoom: 13 });
fetchIsochrone(lat, lng);
});
Mandataire backend (Node.js/Express) :
// /api/geocode, côté serveur uniquement, le code postal ne quitte jamais votre infrastructure
app.get('/api/geocode', async (req, res) => {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', req.query.q);
url.searchParams.set('key', process.env.MAPATLAS_API_KEY); // clé côté serveur
url.searchParams.set('size', '1');
const upstream = await fetch(url.toString());
const data = await upstream.json();
if (!data.features?.length) {
return res.status(404).json({ error: 'Code postal non trouvé' });
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Retournez UNIQUEMENT les coordonnées, aucun code postal enregistré, aucune spécialité en réponse
res.json({ lat, lng });
});
Votre clé API reste sur le serveur et la requête de code postal brut du patient n'est jamais envoyée à un tiers.
Étape 3 : superposition isochrone, « Médecins dans 20 minutes »
Un polygone isochrone montre tous les points accessibles dans un temps de trajet donné. Afficher un sur une recherche de prestataire répond à la question pratique la plus importante du patient : « combien de médecins puis-je réellement atteindre ? »
async function fetchIsochrone(lat, lng) {
// Call through your backend proxy for the same privacy reasons
const res = await fetch(
`/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&profile=driving`
);
const geojson = await res.json();
// Remove existing isochrone layer if present
if (map.getLayer('isochrone-fill')) map.removeLayer('isochrone-fill');
if (map.getLayer('isochrone-border')) map.removeLayer('isochrone-border');
if (map.getSource('isochrone')) map.removeSource('isochrone');
map.addSource('isochrone', { type: 'geojson', data: geojson });
map.addLayer({
id: 'isochrone-fill',
type: 'fill',
source: 'isochrone',
paint: {
'fill-color': '#16A34A',
'fill-opacity': 0.12
}
}, 'provider-markers'); // Insert below markers so pins render on top
map.addLayer({
id: 'isochrone-border',
type: 'line',
source: 'isochrone',
paint: {
'line-color': '#16A34A',
'line-width': 2,
'line-dasharray': [3, 2]
}
}, 'provider-markers');
}
Mandataire backend isochrone :
app.get('/api/isochrone', async (req, res) => {
const { lat, lng, minutes, profile } = req.query;
const url = new URL('https://api.mapatlas.eu/v1/isochrone');
url.searchParams.set('lat', lat);
url.searchParams.set('lng', lng);
url.searchParams.set('time', minutes * 60); // secondes
url.searchParams.set('profile', profile || 'driving');
url.searchParams.set('key', process.env.MAPATLAS_API_KEY);
const upstream = await fetch(url.toString());
const data = await upstream.json();
res.json(data); // polygone GeoJSON
});
L'article Isochrone Maps Explained approfondit les cas d'utilisation des isochrones, y compris les isochrones de transport en commun (utiles pour les patients sans voiture) et les superpositions multi-bandes temporelles montrant les zones de 10, 20 et 30 minutes simultanément.
Étape 4 : Popup de détail du prestataire
Quand un patient clique sur un marqueur de médecin, affichez les détails essentiels : nom, spécialité, statut d'acceptation, langues et prochain rendez-vous disponible.
map.on('click', 'provider-markers', (e) => {
const {
name, specialty, accepting, address, phone, languages, nextAvailable
} = e.features[0].properties;
const coords = e.features[0].geometry.coordinates.slice();
const statusBadge = accepting
? '<span style="color:#16A34A;font-weight:600">Accepting patients</span>'
: '<span style="color:#9CA3AF">Not accepting new patients</span>';
const apptLine = accepting && nextAvailable
? `<p style="margin:4px 0">Next available: <strong>${nextAvailable}</strong></p>`
: '';
new mapmetricsgl.Popup({ maxWidth: '280px' })
.setLngLat(coords)
.setHTML(`
<strong style="font-size:15px">${name}</strong>
<p style="margin:4px 0;text-transform:capitalize">${specialty.replace('-', ' ')}</p>
${statusBadge}
${apptLine}
<p style="margin:6px 0;font-size:13px;color:#64748b">${address}</p>
<p style="margin:4px 0;font-size:13px">
Languages: ${Array.isArray(languages) ? languages.join(', ') : languages}
</p>
<a href="tel:${phone}" style="color:#2563EB">${phone}</a>
`)
.addTo(map);
});
map.on('mouseenter', 'provider-markers', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'provider-markers', () => { map.getCanvas().style.cursor = ''; });
Étape 5 : Panneau de liste synchronisé avec la carte
Une vue liste aux côtés de la carte aide les utilisateurs avec les lecteurs d'écran et ceux qui préfèrent faire défiler plutôt que de cliquer sur les broches.
function buildList(features) {
const list = document.getElementById('provider-list');
list.innerHTML = '';
features.forEach(({ properties, geometry }) => {
const { name, specialty, accepting, address, nextAvailable } = properties;
const card = document.createElement('div');
card.className = `provider-card ${accepting ? 'accepting' : 'full'}`;
card.innerHTML = `
<strong>${name}</strong>
<p>${specialty.replace('-', ' ')}</p>
<p style="font-size:12px;color:#64748b">${address}</p>
<small>${accepting ? `Prochain : ${nextAvailable || 'appeler pour confirmer'}` : 'N'accepte pas'}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 15 });
});
list.appendChild(card);
});
}
Liste de contrôle de mise en place du RGPD
Pour les développeurs de santé européens, traitez cela comme une liste de contrôle de conformité minimale :
- Mandataire backend pour le géocodage : les codes postaux des patients ne doivent pas être envoyés côté client aux API tiers.
- Aucune journalisation de session des termes de recherche : si vous enregistrez les requêtes pour le débogage, supprimez le code postal avant d'écrire dans les journaux.
- Avis de confidentialité : l'avis de confidentialité de votre application doit divulguer que les données de localisation sont traitées pour trouver les prestataires à proximité, la base juridique (intérêt légitime ou consentement explicite) et la période de rétention.
- Consentement pour l'accès à la localisation : si vous utilisez l'API de géolocalisation du navigateur (
navigator.geolocation.getCurrentPosition), affichez un énoncé de but clair avant de déclencher l'invite de permission du navigateur. - Résidence des données : MapAtlas traite toutes les requêtes API dans l'UE. Référencez le DPA MapAtlas lors de la complète de vos dossiers d'activités de traitement.
- Rétention : si vous mettez en cache les résultats isochrones ou recherchez les coordonnées côté serveur pour le performance, définissez une période de rétention (p. ex. 24 heures) et automatisez la suppression.
Le guide du développeur européen pour les API de cartes conformes au RGPD couvre les accords de prestataires, la résidence des données et les modèles de consentement en detail. La page industrie Healthcare Services énumère les fonctionnalités MapAtlas spécifiques pertinentes pour les applications de santé numérique.
Que construire ensuite
- Inscrivez-vous pour une clé API MapAtlas gratuite, une clé pour les cartes, le géocodage, le routage et les isochrones
- Ajoutez le motif didacticiel Store Locator pour les réseaux multi-cliniques avec plus que quelques localisations
- Explorez la page des capacités de l'API de géocodage pour la recherche par code postal, le géocodage inverse et la validation d'adresses de prestataires en masse
Questions fréquemment posées
Pourquoi le RGPD s'applique-t-il à une carte de recherche de médecin même si je ne stocke pas de données de patients ?
La localisation de recherche d'un patient sur une recherche de prestataire de santé révèle où il vit et, par inférence, quelles conditions il peut avoir (p. ex. recherche d'oncologues près d'un code postal). En vertu de l'article 9 du RGPD, les inférences liées à la santé à partir des données de localisation peuvent qualifier de données de catégorie spéciale. Même sans stocker des profils, les requêtes de géocodage en temps réel envoyées côté client aux API tiers créent un dossier de traitement qui nécessite une base juridique et une divulgation.
Qu'est-ce qu'un isochrone et pourquoi est-ce utile pour une recherche de prestataire ?
Un isochrone est un polygone qui montre tous les points accessibles dans un temps de trajet donné à partir d'une localisation de départ. Sur une recherche de prestataire de santé, une superposition isochrone répond à « quels médecins sont à moins de 20 minutes en voiture ? », une question beaucoup plus utile que la distance brute, car elle tient compte des routes, des limites de vitesse et des modèles de circulation.
Puis-je utiliser MapAtlas pour une application NHS ou de santé publique orientée vers les patients ?
Oui. MapAtlas est hébergé en UE, certifié ISO 27001 et fournit un accord de traitement des données conforme au RGPD en vertu de l'article 28. Cela satisfait aux exigences typiques d'approvisionnement du secteur public. UK NHS Digital et les organismes équivalents en Allemagne (DSGVO) et en France (CNIL) exigent la résidence des données dans l'UE/UK, que MapAtlas remplit.

