Um localizador de provedor de saude e um dos mapas mais consequentes que um desenvolvedor pode construir. Pacientes que a usam estao frequentemente ansiosos, com dor ou navegando um sistema desconhecido. Um mapa que carrega lentamente, retorna resultados irrelevantes ou silenciosamente envia seu codigo postal para um servico de terceiros baseado nos EUA nao e apenas uma experiencia de usuario ruim, e uma possivel violacao do RGPD.
Este tutorial constroi um localizador de provedor pronto para producao: filtro de especialidade, pesquisa por codigo postal usando a API de Geocodificacao, sobreposicao de isocrone mostrando "medicos em 20 minutos", e um mapa funcional usando a API de Mapas do MapAtlas, tudo com conformidade com RGPD integrada na arquitetura desde o inicio, nao retrofit afterward.
A implementacao completa e JavaScript simples, nenhum framework necessario. Funciona em qualquer stack: React, Vue, HTML simples, WordPress ou um CMS legado de hospital. O tutorial tambem aborda o contexto regulatorio da EU, dados CQC do Reino Unido, Kassenärztliche Vereinigung da Alemanha e CNAM da Franca, entao voce sabe onde obter dados de provedor legitimamente.
Por que Aplicativos de Localizacao de Saude Teem Obrigacoes Extras de RGPD
Aplicativos padrao coletam dados pessoais (nome, email, uso). Aplicativos de saude teem um segundo risco: inferir informacao de saude a partir de dados de localizacao.
Quando um usuario procura "cardiologistas perto de SE1 7PB", seu codigo postal e dados pessoais. A combinacao de codigo postal e especialidade medica se torna potencialmente dados de categoria especial sob o Artigo 9(1) do RGPD, dados revelando condicao de saude. Isso e verdadeiro mesmo se voce nunca criar uma conta de usuario ou armazenar um perfil.
Os riscos sao concretos:
- Uma chamada de API de geocodificacao do lado do cliente que inclui o codigo postal digitado pelo usuario e enviada para um servidor de terceiros. Esse servidor ve o codigo postal, a especialidade sendo procurada e o endereco IP. Sem um DPA e garantia de residencia de dados da EU, essa e uma transferencia problematica.
- Preenchimento automatico do navegador e historico podem persistir o codigo postal digitado e consulta especialista no dispositivo do usuario, fora de seu controle mas digno de nota em suas informacoes de privacidade.
- Se voce registrar respostas de API do lado do servidor (comum para depuracao), voce pode estar registrando combinacoes de codigo postal + especialidade sem perceber.
A correcao e proxy backend: seu servidor recebe a pesquisa, chama a API de Geocodificacao do MapAtlas e retorna apenas as coordenadas, nunca enviando a consulta digitada para uma API de terceiros do navegador do paciente. Mais sobre isso na secao de implementacao.
Estrutura de Dados de Provedor
Os dados do provedor devem vir de fontes oficiais autoritarias, nao de conjuntos de dados raspados:
- Reino Unido: NHS Choices API do NHS Digital e listas de provedores registrados da CQC
- Alemanha: Dados abertos da Kassenärztliche Bundesvereinigung (KBV) e a API Arztsuche
- Franca: API Ameli do CNAM e o repertorio RPPS
Estruture cada provedor como um recurso 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"
}
}
]
};
Step 1: Map Initialisation with Specialty Filter
Start with the map and a specialty dropdown. The filter updates the visible markers client-side without any network request.
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]);
}
});
Step 2: Postcode Search via Backend Proxy
Rather than calling the Geocoding API from the browser (which exposes the patient's postcode to a third-party server), route the geocoding request through your own backend endpoint. Your server calls MapAtlas and returns only the coordinates.
Frontend (patient's browser):
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);
});
Backend proxy (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 });
});
Your API key stays on the server and the patient's raw postcode query is never sent to a third party.
Step 3: Isochrone Overlay, "Doctors Within 20 Minutes"
An isochrone polygon shows every point reachable within a given drive time. Displaying one on a provider finder answers the most practical patient question: "how many doctors can I actually reach?"
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');
}
Backend isochrone proxy:
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
});
The Isochrone Maps Explained article goes deeper on isochrone use cases, including public transit isochrones (useful for patients without a car) and multi-time-band overlays showing 10, 20, and 30-minute zones simultaneously.
Step 4: Provider Detail Popup
When a patient clicks a doctor marker, show the essential details: name, specialty, accepting status, languages, and next available appointment.
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 = ''; });
Step 5: List Panel Synced with Map
A list view alongside the map helps users with screen readers and those who prefer scrolling to clicking pins.
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);
});
}
GDPR Implementation Checklist
For EU healthcare developers, treat this as a minimum compliance checklist:
- Backend proxy for geocoding: Patient postcodes must not be sent client-side to third-party APIs.
- No session logging of search terms: If you log requests for debugging, strip the postcode before writing to logs.
- Privacy notice: Your app's privacy notice must disclose that location data is processed to find nearby providers, the legal basis (legitimate interest or explicit consent), and the retention period.
- Consent for location access: If you use the browser Geolocation API (
navigator.geolocation.getCurrentPosition), show a clear purpose statement before triggering the browser permission prompt. - Data residency: MapAtlas processes all API requests within the EU. Reference the MapAtlas DPA when completing your Records of Processing Activities.
- Retention: If you cache isochrone results or search coordinates server-side for performance, define a retention period (e.g. 24 hours) and automate deletion.
The EU Developer's Guide to GDPR-Compliant Map APIs covers processor agreements, data residency, and consent patterns in full. The Healthcare Services industry page lists the specific MapAtlas features relevant to digital health applications.
What to Build Next
- Sign up for a free MapAtlas API key, one key for maps, geocoding, routing, and isochrones
- Add the Store Locator tutorial pattern for multi-clinic networks with more than a handful of locations
- Explore the Geocoding API capabilities page for postcode lookup, reverse geocoding, and batch provider address validation
Frequently Asked Questions
Why does GDPR apply to a doctor finder map even if I don't store patient data?
A patient's search location on a healthcare finder reveals where they live and, by inference, which conditions they may have (e.g. searching for oncologists near a postcode). Under GDPR Article 9, health-related inferences from location data can qualify as special category data. Even without storing profiles, real-time geocoding requests sent client-side to third-party APIs create a processing record that requires a legal basis and disclosure.
What is an isochrone and why is it useful for a provider finder?
An isochrone is a polygon that shows every point reachable within a given travel time from a starting location. On a healthcare finder, an isochrone overlay answers "which doctors are within 20 minutes by car?", a much more useful question than raw distance, because it accounts for roads, speed limits, and traffic patterns.
Can I use MapAtlas for a patient-facing NHS or public health application?
Yes. MapAtlas is EU-hosted, ISO 27001 certified, and provides a GDPR-compliant Data Processing Agreement under Article 28. This satisfies typical public sector procurement requirements. UK NHS Digital and equivalent bodies in Germany (DSGVO) and France (CNIL) require data residency within the EU/UK, which MapAtlas fulfils.

