Eine Arztsuche gehört zu den folgenreichsten Karten, die Entwickler bauen können. Patienten, die sie nutzen, sind oft besorgt, haben Schmerzen oder bewegen sich in einem System, das sie nicht kennen. Eine Karte, die langsam lädt, unpassende Ergebnisse liefert oder ihre Postleitzahl unbemerkt an einen Drittanbieter in den USA schickt, ist nicht nur schlechte User Experience. Sie ist ein möglicher DSGVO-Verstoß.
Dieses Tutorial baut eine produktionsreife Arztsuche: Filter nach Fachgebiet, Postleitzahlsuche mit der Geocoding API, ein Isochronen-Overlay für "Ärzte in 20 Minuten erreichbar" und eine funktionierende Karte mit der MapAtlas Maps API. Die DSGVO-Konformität ist dabei von Anfang an Teil der Architektur und wird nicht nachträglich ergänzt.
Die gesamte Implementierung ist Vanilla JavaScript und braucht kein Framework. Sie läuft auf jedem Stack: React, Vue, reines HTML, WordPress oder das Legacy-CMS eines Krankenhauses. Das Tutorial behandelt auch den regulatorischen Rahmen in der EU, Daten der britischen CQC, der deutschen Kassenärztlichen Vereinigung und der französischen CNAM, damit Sie wissen, woher Sie Arztdaten rechtmäßig beziehen.
Warum Standort-Apps im Gesundheitswesen zusätzliche DSGVO-Pflichten haben
Gewöhnliche Apps erheben personenbezogene Daten (Name, E-Mail, Nutzung). Gesundheits-Apps tragen ein zweites Risiko: Rückschlüsse auf Gesundheitsinformationen aus Standortdaten.
Sucht ein Nutzer nach "Kardiologen in der Nähe von SE1 7PB", ist seine Postleitzahl ein personenbezogenes Datum. Die Kombination aus Postleitzahl und medizinischem Fachgebiet kann nach Art. 9 Abs. 1 DSGVO zu einer besonderen Kategorie personenbezogener Daten werden, nämlich zu Gesundheitsdaten, denn der Gerichtshof der EU hat entschieden, dass auch Daten, die sensible Informationen indirekt offenlegen, unter Art. 9 fallen. Das gilt auch dann, wenn Sie nie ein Nutzerkonto anlegen oder ein Profil speichern.
Die Risiken sind konkret:
- Ein clientseitiger Aufruf einer Geocoding-API mit der eingegebenen Postleitzahl geht an den Server eines Dritten. Dieser Server sieht die Postleitzahl, das gesuchte Fachgebiet und die IP-Adresse. Ohne Auftragsverarbeitungsvertrag und Garantie für Datenverarbeitung in der EU ist diese Übermittlung problematisch.
- Autovervollständigung und Verlauf des Browsers können die eingegebene Postleitzahl und die Suche nach dem Facharzt auf dem Gerät des Nutzers speichern. Das liegt außerhalb Ihrer Kontrolle, gehört aber in Ihre Datenschutzhinweise.
- Wenn Sie API-Antworten serverseitig protokollieren (beim Debugging üblich), speichern Sie unter Umständen Kombinationen aus Postleitzahl und Fachgebiet, ohne es zu merken.
Die Lösung ist ein Backend-Proxy: Ihr Server nimmt die Suche entgegen, ruft die MapAtlas Geocoding API auf und gibt nur die Koordinaten zurück. Die eingegebene Suchanfrage geht nie aus dem Browser des Patienten an die API eines Drittanbieters. Mehr dazu im Abschnitt zur Implementierung.
Datenstruktur für Arztdaten
Arztdaten sollten aus maßgeblichen offiziellen Quellen stammen, nicht aus gescrapten Datensätzen:
- Vereinigtes Königreich: die Directory of Healthcare Services (Service Search) API von NHS England und die Listen der bei der CQC registrierten Anbieter
- Deutschland: Open Data der Kassenärztlichen Bundesvereinigung (KBV) und die Arztsuche API
- Frankreich: die Ameli API der CNAM und das Répertoire RPPS
Legen Sie jeden Anbieter als GeoJSON-Feature an:
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"
}
}
]
};
Schritt 1: Karte mit Fachgebiet-Filter initialisieren
Beginnen Sie mit der Karte und einem Dropdown für das Fachgebiet. Der Filter aktualisiert die sichtbaren Marker clientseitig, ganz ohne Netzwerkanfrage.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'provider-map',
style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
map.on('load', () => {
map.addSource('providers', {
type: 'geojson',
data: providers
});
// Arzt-Marker, grün = nimmt Patienten auf, grau = ausgelastet
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);
});
// Filter nach Fachgebiet
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]);
}
});
Schritt 2: Postleitzahlsuche über einen Backend-Proxy
Rufen Sie die Geocoding API nicht aus dem Browser auf, denn damit landet die Postleitzahl des Patienten auf dem Server eines Dritten. Leiten Sie die Anfrage stattdessen über einen eigenen Backend-Endpunkt. Ihr Server ruft MapAtlas auf und gibt nur die Koordinaten zurück.
Frontend (Browser des Patienten):
async function geocodePostcode(postcode) {
// IHR Backend aufrufen, nicht direkt die Geocoding API
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, nur serverseitig, die Postleitzahl verlässt nie Ihre Infrastruktur
app.get('/api/geocode', async (req, res) => {
const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
url.searchParams.set('text', req.query.q);
url.searchParams.set('token', process.env.MAPATLAS_API_KEY); // serverseitiger Schlüssel
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;
// NUR Koordinaten zurückgeben, keine Postleitzahl protokollieren, kein Fachgebiet in der Antwort
res.json({ lat, lng });
});
Ihr API-Schlüssel bleibt auf dem Server, der Browser des Patienten kontaktiert den Geocoder nie direkt, und Ihr Server gibt nur die Postleitzahl selbst weiter, ohne das Fachgebiet und ohne Angaben, die den Patienten identifizieren.
Schritt 3: Isochronen-Overlay, "Ärzte in 20 Minuten erreichbar"
Ein Isochronen-Polygon zeigt jeden Punkt, der innerhalb einer bestimmten Fahrzeit erreichbar ist. In einer Arztsuche beantwortet es die praktischste Frage des Patienten: "Wie viele Ärzte kann ich tatsächlich erreichen?"
async function fetchIsochrone(lat, lng) {
// Aus denselben Datenschutzgründen über den Backend-Proxy aufrufen
const res = await fetch(
`/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&costing=auto`
);
const geojson = await res.json();
// Vorhandenen Isochronen-Layer entfernen, falls vorhanden
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'); // Unter den Markern einfügen, damit die Pins oben liegen
map.addLayer({
id: 'isochrone-border',
type: 'line',
source: 'isochrone',
paint: {
'line-color': '#16A34A',
'line-width': 2,
'line-dasharray': [3, 2]
}
}, 'provider-markers');
}
Backend-Proxy für Isochronen:
app.get('/api/isochrone', async (req, res) => {
const { lat, lng, minutes, costing } = req.query;
const url = new URL('https://gateway.mapmetrics-atlas.net/isochrone/');
url.searchParams.set('token', process.env.MAPATLAS_API_KEY);
const upstream = await fetch(url.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
locations: [{ lat: Number(lat), lon: Number(lng) }],
costing: costing || 'auto',
contours: [{ time: Number(minutes) }],
polygons: true,
}),
});
const data = await upstream.json();
res.json(data); // GeoJSON-Polygon
});
Der Artikel Isochronen-Karten erklärt geht tiefer auf Anwendungsfälle ein, darunter Isochronen für den öffentlichen Nahverkehr (hilfreich für Patienten ohne Auto) und Overlays mit mehreren Zeitbändern, die Zonen für 10, 20 und 30 Minuten gleichzeitig zeigen.
Schritt 4: Popup mit Arztdetails
Klickt ein Patient auf einen Arzt-Marker, zeigen Sie die wichtigsten Angaben: Name, Fachgebiet, ob neue Patienten aufgenommen werden, Sprachen und den nächsten freien Termin.
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 = ''; });
Schritt 5: Mit der Karte synchronisierte Liste
Eine Listenansicht neben der Karte hilft Nutzern mit Screenreadern und allen, die lieber scrollen als Pins anzuklicken.
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);
});
}
Die Liste erscheint in der Reihenfolge, in der Ihre Arztdaten eintreffen. Um sie danach zu sortieren, wie lange die Anfahrt zu jedem Arzt tatsächlich dauert, schickt Ihr Backend den Ausgangspunkt der Suche und die gefilterten Anbieter an die Distance Matrix API. Sortieren Sie die Karten anschließend nach den zurückgegebenen Fahrzeiten. Leiten Sie die Anfrage über denselben serverseitigen Proxy wie das Geocoding, aus dem Grund in der Checkliste unten, dann wird sie innerhalb der EU verarbeitet.
DSGVO-Checkliste für die Implementierung
Entwickler im europäischen Gesundheitswesen sollten diese Checkliste als Mindestanforderung behandeln:
- Backend-Proxy für Geocoding: Postleitzahlen von Patienten dürfen nicht clientseitig an APIs von Drittanbietern gehen.
- Keine Protokollierung von Suchbegriffen in Sessions: Wenn Sie Anfragen zum Debugging protokollieren, entfernen Sie die Postleitzahl, bevor Sie in die Logs schreiben.
- Datenschutzhinweis: Der Datenschutzhinweis Ihrer App muss offenlegen, dass Standortdaten zur Suche nach Ärzten in der Nähe verarbeitet werden, auf welcher Rechtsgrundlage nach Art. 6 DSGVO dies geschieht, welche Bedingung nach Art. 9 Abs. 2 greift (etwa eine ausdrückliche Einwilligung), wo Suchdaten Gesundheitsinformationen offenlegen können, und wie lange die Daten gespeichert werden.
- Einwilligung für den Standortzugriff: Wenn Sie die Geolocation API des Browsers (
navigator.geolocation.getCurrentPosition) nutzen, erklären Sie den Zweck deutlich, bevor Sie die Berechtigungsabfrage des Browsers auslösen. - Datenverarbeitung in der EU: MapAtlas verarbeitet alle API-Anfragen innerhalb der EU. Verweisen Sie auf den MapAtlas-Auftragsverarbeitungsvertrag, wenn Sie Ihr Verzeichnis von Verarbeitungstätigkeiten ausfüllen.
- Speicherdauer: Wenn Sie Isochronen-Ergebnisse oder Suchkoordinaten serverseitig aus Performancegründen cachen, legen Sie eine Speicherdauer fest (z. B. 24 Stunden) und automatisieren Sie die Löschung.
Der Leitfaden für EU-Entwickler zu DSGVO-konformen Karten-APIs behandelt Auftragsverarbeitungsverträge, Datenverarbeitung in der EU und Muster für Einwilligungen ausführlich. Die Branchenseite für Gesundheitsdienstleistungen listet die MapAtlas-Funktionen auf, die für digitale Gesundheitsanwendungen relevant sind.
Wie es weitergeht
- Kostenlosen MapAtlas API-Schlüssel anlegen: ein Schlüssel für Karten, Geocoding, Routing und Isochronen
- Übernehmen Sie das Muster aus dem Store-Locator-Tutorial für Praxisverbünde mit mehr als einer Handvoll Standorten
- Die Seite zu den Funktionen der Geocoding API zeigt Postleitzahlsuche, Reverse Geocoding und die Validierung von Arztadressen im Batch
Häufig gestellte Fragen
Warum gilt die DSGVO für eine Arztsuche, auch wenn ich keine Patientendaten speichere?
Der Suchstandort eines Patienten in einer Arztsuche verrät, wo er wohnt, und lässt Rückschlüsse darauf zu, welche Erkrankungen er haben könnte (etwa bei der Suche nach Onkologen in der Nähe einer Postleitzahl). Nach Art. 9 DSGVO können gesundheitsbezogene Rückschlüsse aus Standortdaten als besondere Kategorie personenbezogener Daten gelten. Auch ohne gespeicherte Profile entsteht durch clientseitige Geocoding-Anfragen in Echtzeit an APIs von Drittanbietern eine Verarbeitung, die eine Rechtsgrundlage und Transparenz erfordert.
Was ist eine Isochrone und warum ist sie für eine Arztsuche nützlich?
Eine Isochrone ist ein Polygon, das jeden Punkt zeigt, der von einem Startort aus innerhalb einer bestimmten Reisezeit erreichbar ist. In einer Arztsuche beantwortet ein Isochronen-Overlay die Frage "Welche Ärzte erreiche ich mit dem Auto in 20 Minuten?". Diese Frage ist deutlich nützlicher als die reine Entfernung, weil sie Straßen, Tempolimits und Verkehrsmuster berücksichtigt.
Kann ich MapAtlas für eine patientenorientierte Anwendung des NHS oder des öffentlichen Gesundheitswesens nutzen?
Ja. MapAtlas wird in der EU in ISO-27001-zertifizierten Rechenzentren gehostet und bietet einen DSGVO-konformen Auftragsverarbeitungsvertrag nach Art. 28. Das erfüllt die typischen Anforderungen öffentlicher Vergabeverfahren. Da die Verarbeitung in der EU bleibt, entfallen außerdem die Fragen zu internationalen Datenübermittlungen, die Vergabeteams im öffentlichen Sektor und im Gesundheitswesen routinemäßig stellen.
Häufig gestellte Fragen
Warum gilt die DSGVO für eine Arztsuche, auch wenn ich keine Patientendaten speichere?
Der Suchstandort eines Patienten in einer Arztsuche verrät, wo er wohnt, und lässt Rückschlüsse darauf zu, welche Erkrankungen er haben könnte (etwa bei der Suche nach Onkologen in der Nähe einer Postleitzahl). Nach Art. 9 DSGVO können gesundheitsbezogene Rückschlüsse aus Standortdaten als besondere Kategorie personenbezogener Daten gelten. Auch ohne gespeicherte Profile entsteht durch clientseitige Geocoding-Anfragen in Echtzeit an APIs von Drittanbietern eine Verarbeitung, die eine Rechtsgrundlage und Transparenz erfordert.
Was ist eine Isochrone und warum ist sie für eine Arztsuche nützlich?
Eine Isochrone ist ein Polygon, das jeden Punkt zeigt, der von einem Startort aus innerhalb einer bestimmten Reisezeit erreichbar ist. In einer Arztsuche beantwortet ein Isochronen-Overlay die Frage „Welche Ärzte erreiche ich mit dem Auto in 20 Minuten?“. Diese Frage ist deutlich nützlicher als die reine Entfernung, weil sie Straßen, Tempolimits und Verkehrsmuster berücksichtigt.
Kann ich MapAtlas für eine patientenorientierte Anwendung des NHS oder des öffentlichen Gesundheitswesens nutzen?
Ja. MapAtlas wird in der EU in ISO-27001-zertifizierten Rechenzentren gehostet und bietet einen DSGVO-konformen Auftragsverarbeitungsvertrag nach Art. 28. Das erfüllt die typischen Anforderungen öffentlicher Vergabeverfahren. Da die Verarbeitung in der EU bleibt, entfallen außerdem die Fragen zu internationalen Datenübermittlungen, die Vergabeteams im öffentlichen Sektor und im Gesundheitswesen routinemäßig stellen.

