Una mappa per trovare operatori sanitari è una delle mappe più importanti che uno sviluppatore possa costruire. I pazienti che la utilizzano sono spesso ansiosi, sofferenti o alle prese con un sistema che non conoscono. Una mappa che si carica lentamente, restituisce risultati irrilevanti o invia silenziosamente il loro codice postale a un servizio di terze parti con sede negli USA non è solo una brutta esperienza utente: è una potenziale violazione del GDPR.
Questo tutorial costruisce un provider finder pronto per la produzione: filtro per specialità, ricerca per codice postale tramite la Geocoding API, overlay isocrono che mostra "medici entro 20 minuti" e una mappa funzionante con la MapAtlas Maps API, il tutto con la conformità GDPR integrata nell'architettura fin dall'inizio, non aggiunta in un secondo momento.
L'implementazione completa è in vanilla JavaScript, senza framework richiesti. Funziona su qualsiasi stack: React, Vue, HTML semplice, WordPress o il CMS legacy di un ospedale. Il tutorial copre anche il contesto normativo UE, i dati CQC del Regno Unito, la Kassenärztliche Vereinigung tedesca e la CNAM francese, così sai dove ottenere i dati dei provider in modo legittimo.
Perché le App Sanitarie Hanno Obblighi GDPR Aggiuntivi
Le app standard raccolgono dati personali (nome, email, utilizzo). Le app sanitarie hanno un secondo rischio: dedurre informazioni sanitarie dai dati di posizione.
Quando un utente cerca "cardiologi vicino a SE1 7PB", il suo codice postale è un dato personale. La combinazione di codice postale e specialità medica diventa potenzialmente un dato di categoria speciale ai sensi dell'Articolo 9(1) del GDPR, ossia dati che rivelano condizioni di salute. Questo vale anche se non si crea mai un account utente o si archivia un profilo.
I rischi sono concreti:
- Una chiamata API di geocodifica lato client che include il codice postale digitato dall'utente viene inviata a un server di terze parti. Quel server vede il codice postale, la specialità cercata e l'indirizzo IP. Senza un DPA e una garanzia di residenza dei dati nell'UE, questo costituisce un trasferimento problematico.
- Il completamento automatico e la cronologia del browser possono persistere il codice postale digitato e la query dello specialista sul dispositivo dell'utente, fuori dal tuo controllo ma vale la pena menzionarlo nelle tue informazioni sulla privacy.
- Se registri le risposte API lato server (comune per il debug), potresti registrare combinazioni codice postale + specialità senza rendertene conto.
La soluzione è il proxy backend: il tuo server riceve la ricerca, chiama la Geocoding API di MapAtlas e restituisce solo le coordinate, senza mai inviare la query digitata a un'API di terze parti dal browser del paziente. Maggiori dettagli nella sezione sull'implementazione.
Struttura dei Dati del Provider
I dati dei provider devono provenire da fonti ufficiali autorevoli, non da dataset raschiati:
- UK: NHS Choices API di NHS Digital e liste di provider registrati CQC
- Germania: Dati aperti della Kassenärztliche Bundesvereinigung (KBV) e l'Arztsuche API
- Francia: Ameli API della CNAM e il répertoire RPPS
Struttura ogni provider come una feature 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"
}
}
]
};
Passo 1: Inizializzazione della Mappa con Filtro per Specialità
Inizia con la mappa e un menu a tendina per le specialità. Il filtro aggiorna i marker visibili lato client senza alcuna richiesta di rete.
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]);
}
});
Passo 2: Ricerca per Codice Postale tramite Proxy Backend
Invece di chiamare la Geocoding API dal browser (che espone il codice postale del paziente a un server di terze parti), instrada la richiesta di geocodifica tramite il tuo endpoint backend. Il tuo server chiama MapAtlas e restituisce solo le coordinate.
Frontend (browser del paziente):
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);
});
Proxy backend (Node.js/Express):
// /api/geocode, server-side only, postcode never leaves your 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); // server-side key
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: 'Postcode not found' });
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Return ONLY coordinates, no postcode logged, no specialty in response
res.json({ lat, lng });
});
La tua chiave API rimane sul server e la query grezza del codice postale del paziente non viene mai inviata a terze parti.
Passo 3: Overlay Isocrono, "Medici Entro 20 Minuti"
Un poligono isocrono mostra ogni punto raggiungibile entro un determinato tempo di guida. Visualizzarne uno su un provider finder risponde alla domanda pratica più comune del paziente: "quanti medici posso effettivamente raggiungere?"
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');
}
Proxy isocronico backend:
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); // seconds
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); // GeoJSON polygon
});
L'articolo Mappe Isocroniche Spiegate approfondisce i casi d'uso delle isocrone, incluse le isocrone per i trasporti pubblici (utili per i pazienti senza auto) e gli overlay multi-banda temporale che mostrano simultaneamente le zone a 10, 20 e 30 minuti.
Passo 4: Popup con i Dettagli del Provider
Quando un paziente clicca su un marker di un medico, mostra i dettagli essenziali: nome, specialità, stato di accettazione, lingue e prossimo appuntamento disponibile.
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 = ''; });
Passo 5: Pannello Lista Sincronizzato con la Mappa
Una vista a lista accanto alla mappa aiuta gli utenti con screen reader e quelli che preferiscono scorrere al posto di cliccare sui segnaposto.
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 ? `Next: ${nextAvailable || 'call to confirm'}` : 'Not accepting'}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 15 });
});
list.appendChild(card);
});
}
Checklist di Implementazione GDPR
Per gli sviluppatori sanitari UE, tratta questo come una checklist minima di conformità:
- Proxy backend per la geocodifica: I codici postali dei pazienti non devono essere inviati lato client ad API di terze parti.
- Nessun log di sessione dei termini di ricerca: Se registri le richieste per il debug, rimuovi il codice postale prima di scrivere nei log.
- Informativa sulla privacy: L'informativa sulla privacy della tua app deve dichiarare che i dati di posizione vengono elaborati per trovare provider nelle vicinanze, la base giuridica (interesse legittimo o consenso esplicito) e il periodo di conservazione.
- Consenso per l'accesso alla posizione: Se usi la Geolocation API del browser (
navigator.geolocation.getCurrentPosition), mostra una chiara dichiarazione di scopo prima di attivare il prompt di autorizzazione del browser. - Residenza dei dati: MapAtlas elabora tutte le richieste API nell'UE. Fai riferimento al MapAtlas DPA quando completi i tuoi Registri delle Attività di Trattamento.
- Conservazione: Se metti in cache i risultati isocronici o le coordinate di ricerca lato server per le prestazioni, definisci un periodo di conservazione (es. 24 ore) e automatizza la cancellazione.
La Guida per Sviluppatori UE alle Map API Conformi al GDPR copre in modo completo gli accordi con i processori, la residenza dei dati e i modelli di consenso. La pagina del settore Healthcare Services elenca le funzionalità specifiche di MapAtlas rilevanti per le applicazioni di salute digitale.
Cosa Costruire Dopo
- Iscriviti per una chiave API MapAtlas gratuita: una chiave per mappe, geocodifica, routing e isocrone
- Aggiungi il pattern del tutorial Store Locator per reti multi-clinica con più di qualche sede
- Esplora la pagina delle capacità della Geocoding API per la ricerca per codice postale, la geocodifica inversa e la validazione batch degli indirizzi dei provider
Domande Frequenti
Perché il GDPR si applica a una mappa per trovare medici anche se non archivio dati dei pazienti?
La posizione di ricerca di un paziente su un healthcare finder rivela dove vive e, per inferenza, quali condizioni potrebbe avere (es. cercando oncologi vicino a un codice postale). Ai sensi dell'Articolo 9 del GDPR, le inferenze relative alla salute dai dati di posizione possono qualificarsi come dati di categoria speciale. Anche senza archiviare profili, le richieste di geocodifica in tempo reale inviate lato client ad API di terze parti creano un registro di trattamento che richiede una base giuridica e una disclosure.
Cos'è un'isocrona e perché è utile per un provider finder?
Un'isocrona è un poligono che mostra ogni punto raggiungibile entro un determinato tempo di percorrenza da una posizione di partenza. Su un healthcare finder, un overlay isocrono risponde alla domanda "quali medici sono raggiungibili entro 20 minuti in auto?", una domanda molto più utile della distanza grezza, perché tiene conto di strade, limiti di velocità e modelli di traffico.
Posso usare MapAtlas per un'applicazione NHS o di sanità pubblica rivolta ai pazienti?
Sì. MapAtlas è ospitato nell'UE, certificato ISO 27001 e fornisce un Data Processing Agreement conforme al GDPR ai sensi dell'Articolo 28. Questo soddisfa i requisiti tipici di procurement del settore pubblico. NHS Digital nel Regno Unito e gli organismi equivalenti in Germania (DSGVO) e Francia (CNIL) richiedono la residenza dei dati nell'UE/UK, che MapAtlas garantisce.

