Skip to main content
Cach Xay Dung Ban Do Tim Kiem Nha Cung Cap Dich Vu Y Te Tuan Thu GDPR
Tutorials

Cach Xay Dung Ban Do Tim Kiem Nha Cung Cap Dich Vu Y Te Tuan Thu GDPR

Xay dung ban do tim kiem bac si tuan thu GDPR voi bo loc chuyen khoa, tim kiem ma buu chinh, lop phu isochrone thoi gian di chuyen va quyen rieng tu benh nhan theo thiet

Brent van der Heiden10 min read
#healthcare map#provider finder#doctor finder#healthcare api#gdpr healthcare#health app

Một bản đồ tìm nhà cung cấp dịch vụ chăm sóc sức khỏe là một trong những bản đồ có hậu quả nhất mà nhà phát triển có thể xây dựng. Bệnh nhân sử dụng nó thường lo lắng, bị đau hoặc đang điều hướng qua một hệ thống không quen thuộc. Một bản đồ tải chậm, trả về kết quả không liên quan hoặc im lặng gửi mã bưu chính của họ đến dịch vụ bên thứ ba có trụ sở ở Mỹ không chỉ là trải nghiệm người dùng tồi tệ, mà còn là một vi phạm GDPR tiềm ẩn.

Hướng dẫn này xây dựng một bộ tìm nhà cung cấp sẵn sàng sản xuất: bộ lọc chuyên khoa, tìm kiếm mã bưu chính bằng Geocoding API, lớp phủ isochrone hiển thị "bác sĩ trong 20 phút", và một bản đồ hoạt động bằng cách sử dụng MapAtlas Maps API, tất cả đều tuân thủ GDPR được xây dựng vào kiến trúc từ đầu, không phải được cải tạo sau đó.

Việc triển khai đầy đủ là JavaScript vanilla, không cần khung. Nó hoạt động trên bất kỳ ngăn xếp nào: React, Vue, HTML thuần, WordPress hoặc hệ thống quản lý nội dung kế thừa của bệnh viện. Hướng dẫn này cũng bao gồm bối cảnh quy định EU, dữ liệu CQC của Anh, Kassenärztliche Vereinigung Đức và CNAM Pháp, vì vậy bạn biết nơi để lấy dữ liệu nhà cung cấp hợp pháp.

Tại sao các ứng dụng vị trí chăm sóc sức khỏe có nghĩa vụ GDPR bổ sung

Các ứng dụng tiêu chuẩn thu thập dữ liệu cá nhân (tên, email, sử dụng). Các ứng dụng chăm sóc sức khỏe có rủi ro thứ hai: suy luận thông tin sức khỏe từ dữ liệu vị trí.

Khi người dùng tìm kiếm "các bác sĩ tim mạch gần SE1 7PB", mã bưu chính của họ là dữ liệu cá nhân. Sự kết hợp của mã bưu chính và chuyên khoa y tế trở thành dữ liệu danh mục đặc biệt tiềm ẩn theo Điều 9 (1) GDPR, dữ liệu tiết lộ tình trạng sức khỏe. Điều này đúng ngay cả khi bạn không bao giờ tạo tài khoản người dùng hoặc lưu trữ hồ sơ.

The risks are concrete:

  • A client-side geocoding API call that includes the user's typed postcode is sent to a third-party server. That server sees the postcode, the specialty being searched, and the IP address. Without a DPA and an EU data residency guarantee, that is a problematic transfer.
  • Browser autocomplete and history may persist the typed postcode and specialist query on the user's device, outside your control but worth noting in your privacy information.
  • If you log API responses server-side (common for debugging), you may be logging postcode + specialty combinations without realising it.

The fix is backend proxying: your server receives the search, calls the MapAtlas Geocoding API, and returns only the coordinates, never sending the typed query to a third-party API from the patient's browser. More on this in the implementation section.

Provider Data Structure

Provider data should come from authoritative official sources, not scraped datasets:

  • UK: NHS Digital's NHS Choices API and CQC registered provider lists
  • Germany: Kassenärztliche Bundesvereinigung (KBV) open data and the Arztsuche API
  • France: CNAM's Ameli API and the répertoire RPPS

Structure each provider as a GeoJSON feature:

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

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 in ISO 27001 certified data centres, 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.

Thấy hữu ích? Chia sẻ nhé.

Về tác giả

Brent van der Heiden

Viết bởi

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. He writes about geospatial infrastructure, AI search visibility, and how location data powers the products people rely on every day.

Xem tất cả bài viết
Quay lại blog