Skip to main content
Tutorials

Como construir um mapa de provedores de saúde conforme o GDPR

Construa um localizador de médicos em conformidade com o GDPR: filtro de especialidade, busca por CEP, isócronas de tempo de viagem e privacidade do paciente.

Brent van der Heiden12 min read
#healthcare map#provider finder#doctor finder#healthcare api#gdpr healthcare#health app
Como construir um mapa de provedores de saúde conforme o GDPR

Um localizador de profissionais de saúde é um dos mapas de maior impacto que um desenvolvedor pode construir. Quem usa esse tipo de ferramenta muitas vezes está ansioso, com dor ou tentando se orientar em um sistema desconhecido. Um mapa que carrega devagar, retorna resultados irrelevantes ou envia silenciosamente o CEP do paciente para um serviço de terceiros sediado nos EUA não é só uma experiência ruim: é uma possível violação do GDPR.

Este tutorial constrói um localizador de profissionais pronto para produção: filtro por especialidade, busca por código postal com a API de Geocodificação, sobreposição de isócrona mostrando "médicos a até 20 minutos" e um mapa funcional com a API de Mapas do MapAtlas. Tudo com a conformidade com o GDPR incorporada à arquitetura desde o início, e não adaptada depois.

A implementação completa usa JavaScript puro, sem framework. Ela funciona em qualquer stack: React, Vue, HTML simples, WordPress ou o CMS legado de um hospital. O tutorial também cobre o contexto regulatório europeu, com os dados da CQC no Reino Unido, da Kassenärztliche Vereinigung na Alemanha e da CNAM na França, para que você saiba onde obter dados de profissionais de forma legítima.

Por que apps de localização na saúde têm obrigações extras no GDPR

Apps comuns coletam dados pessoais (nome, e-mail, dados de uso). Apps de saúde têm um segundo risco: inferir informações de saúde a partir de dados de localização.

Quando um usuário busca "cardiologistas perto de SE1 7PB", o código postal dele é um dado pessoal. A combinação de código postal com especialidade médica pode se tornar um dado de categoria especial segundo o Artigo 9(1) do GDPR, ou seja, um dado relativo à saúde, porque o Tribunal de Justiça da UE decidiu que dados que revelam informações sensíveis de forma indireta se enquadram no Artigo 9. Isso vale mesmo que você nunca crie uma conta de usuário nem armazene um perfil.

Os riscos são concretos:

  • Uma chamada à API de geocodificação feita no cliente, com o código postal digitado pelo usuário, é enviada a um servidor de terceiros. Esse servidor vê o código postal, a especialidade buscada e o endereço IP. Sem um DPA e sem garantia de residência dos dados na UE, essa é uma transferência problemática.
  • O preenchimento automático e o histórico do navegador podem guardar o código postal e a busca por especialista no dispositivo do usuário. Isso está fora do seu controle, mas vale mencionar nas suas informações de privacidade.
  • Se você registra respostas da API no servidor (algo comum para depuração), pode estar gravando combinações de código postal + especialidade sem perceber.

A solução é um proxy no backend: seu servidor recebe a busca, chama a API de Geocodificação do MapAtlas e devolve apenas as coordenadas. A consulta digitada nunca é enviada do navegador do paciente para uma API de terceiros. Veja os detalhes na seção de implementação.

Estrutura dos dados de profissionais

Os dados dos profissionais devem vir de fontes oficiais e confiáveis, não de datasets obtidos por scraping:

  • Reino Unido: a Directory of Healthcare Services (Service Search) API do NHS England e as listas de prestadores registrados na CQC
  • Alemanha: os dados abertos da Kassenärztliche Bundesvereinigung (KBV) e a API Arztsuche
  • França: a API Ameli da CNAM e o répertoire RPPS

Estruture cada profissional como uma 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: inicialize o mapa com filtro por especialidade

Comece com o mapa e um menu suspenso de especialidades. O filtro atualiza os marcadores visíveis no cliente, sem nenhuma requisição de rede.

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
  });

  // Marcadores de profissionais: verde = aceitando pacientes, cinza = agenda cheia
  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);
});

// Filtro por especialidade
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: busca por código postal com proxy no backend

Em vez de chamar a API de Geocodificação a partir do navegador (o que expõe o código postal do paciente a um servidor de terceiros), encaminhe a requisição de geocodificação por um endpoint do seu próprio backend. Seu servidor chama o MapAtlas e devolve apenas as coordenadas.

Frontend (navegador do paciente):

async function geocodePostcode(postcode) {
  // Chame o SEU backend, não a API de geocodificação diretamente
  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 no backend (Node.js/Express):

// /api/geocode, somente no servidor: o código postal nunca sai da sua infraestrutura
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); // chave do lado do servidor
  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;
  // Retorna SOMENTE as coordenadas: nenhum código postal registrado, nenhuma especialidade na resposta
  res.json({ lat, lng });
});

Sua chave de API fica no servidor, o navegador do paciente nunca se comunica diretamente com o geocodificador, e a única coisa que o seu servidor repassa é o próprio código postal, sem a especialidade nem nada que identifique o paciente.

Passo 3: sobreposição de isócrona com "médicos a até 20 minutos"

Um polígono de isócrona mostra todos os pontos alcançáveis dentro de um tempo de viagem definido. Exibi-lo em um localizador de profissionais responde à pergunta mais prática do paciente: "quantos médicos eu consigo realmente alcançar?"

async function fetchIsochrone(lat, lng) {
  // Chame pelo proxy do backend, pelos mesmos motivos de privacidade
  const res = await fetch(
    `/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&costing=auto`
  );
  const geojson = await res.json();

  // Remove a camada de isócrona existente, se houver
  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'); // Insere abaixo dos marcadores para que os pinos fiquem por cima

  map.addLayer({
    id: 'isochrone-border',
    type: 'line',
    source: 'isochrone',
    paint: {
      'line-color': '#16A34A',
      'line-width': 2,
      'line-dasharray': [3, 2]
    }
  }, 'provider-markers');
}

Proxy de isócrona no backend:

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); // polígono GeoJSON
});

O artigo Mapas de isócronas explicados aprofunda os casos de uso de isócronas, incluindo isócronas de transporte público (úteis para pacientes sem carro) e sobreposições com várias faixas de tempo, mostrando zonas de 10, 20 e 30 minutos ao mesmo tempo.

Passo 4: popup com detalhes do profissional

Quando o paciente clicar no marcador de um médico, mostre os dados essenciais: nome, especialidade, se está aceitando novos pacientes, idiomas e próxima data disponível.

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: painel de lista sincronizado com o mapa

Uma lista ao lado do mapa ajuda quem usa leitor de tela e quem prefere rolar uma lista a clicar em pinos.

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);
  });
}

A lista aparece na ordem em que os dados dos profissionais chegam. Para ordená-la pelo tempo real de deslocamento até cada profissional, faça seu backend enviar a origem da busca e o conjunto filtrado de profissionais para a API Distance Matrix e ordene os cards pelos tempos de viagem retornados. Encaminhe essa chamada pelo mesmo proxy no servidor usado na geocodificação, pelo motivo explicado no checklist abaixo, e a requisição será processada dentro da UE.

Checklist de implementação do GDPR

Para quem desenvolve para a saúde na UE, trate esta lista como o mínimo de conformidade:

  • Proxy no backend para geocodificação: os códigos postais dos pacientes não devem ser enviados do cliente para APIs de terceiros.
  • Sem registro dos termos de busca: se você registra requisições para depuração, remova o código postal antes de gravar os logs.
  • Aviso de privacidade: o aviso de privacidade do seu app precisa informar que dados de localização são processados para encontrar profissionais próximos, qual é a base legal segundo o Artigo 6 e, quando os dados de busca puderem revelar informações de saúde, qual condição do Artigo 9(2) se aplica, como o consentimento explícito, além do período de retenção.
  • Consentimento para acesso à localização: se você usa a Geolocation API do navegador (navigator.geolocation.getCurrentPosition), mostre uma explicação clara da finalidade antes de disparar o pedido de permissão do navegador.
  • Residência dos dados: o MapAtlas processa todas as requisições de API dentro da UE. Cite o DPA do MapAtlas ao preencher seu Registro das Atividades de Tratamento.
  • Retenção: se você armazena em cache no servidor os resultados de isócronas ou as coordenadas de busca para ganhar desempenho, defina um período de retenção (por exemplo, 24 horas) e automatize a exclusão.

O guia para desenvolvedores europeus sobre APIs de mapas em conformidade com o GDPR cobre em detalhes os contratos com operadores, a residência dos dados e os padrões de consentimento. A página do setor de serviços de saúde lista os recursos do MapAtlas mais relevantes para aplicações de saúde digital.

O que construir a seguir

Perguntas frequentes

Por que o GDPR se aplica a um mapa de busca de médicos mesmo que eu não armazene dados de pacientes?

O local buscado por um paciente em um localizador de saúde revela onde ele mora e, por inferência, quais condições ele pode ter (por exemplo, ao buscar oncologistas perto de um código postal). Segundo o Artigo 9 do GDPR, inferências relacionadas à saúde a partir de dados de localização podem ser consideradas dados de categoria especial. Mesmo sem armazenar perfis, requisições de geocodificação em tempo real enviadas do cliente para APIs de terceiros geram um tratamento de dados que exige base legal e transparência.

O que é uma isócrona e por que ela é útil em um localizador de profissionais?

Uma isócrona é um polígono que mostra todos os pontos alcançáveis dentro de um tempo de viagem definido a partir de um ponto de partida. Em um localizador de saúde, a sobreposição de isócrona responde "quais médicos estão a até 20 minutos de carro?", uma pergunta muito mais útil que a distância bruta, porque considera as vias, os limites de velocidade e os padrões de trânsito.

Posso usar o MapAtlas em uma aplicação do NHS ou de saúde pública voltada a pacientes?

Sim. O MapAtlas é hospedado na UE em data centers com certificação ISO 27001 e oferece um Contrato de Processamento de Dados (DPA) em conformidade com o GDPR, nos termos do Artigo 28. Isso atende aos requisitos típicos de compras do setor público. Manter o processamento dentro da UE também elimina as perguntas sobre transferências internacionais que as equipes de compras do setor público e da saúde fazem rotineiramente.

Perguntas frequentes

Por que o GDPR se aplica a um mapa de busca de médicos mesmo que eu não armazene dados de pacientes?

O local buscado por um paciente em um localizador de saúde revela onde ele mora e, por inferência, quais condições ele pode ter (por exemplo, ao buscar oncologistas perto de um código postal). Segundo o Artigo 9 do GDPR, inferências relacionadas à saúde a partir de dados de localização podem ser consideradas dados de categoria especial. Mesmo sem armazenar perfis, requisições de geocodificação em tempo real enviadas do cliente para APIs de terceiros geram um tratamento de dados que exige base legal e transparência.

O que é uma isócrona e por que ela é útil em um localizador de profissionais?

Uma isócrona é um polígono que mostra todos os pontos alcançáveis dentro de um tempo de viagem definido a partir de um ponto de partida. Em um localizador de saúde, a sobreposição de isócrona responde “quais médicos estão a até 20 minutos de carro?”, uma pergunta muito mais útil que a distância bruta, porque considera as vias, os limites de velocidade e os padrões de trânsito.

Posso usar o MapAtlas em uma aplicação do NHS ou de saúde pública voltada a pacientes?

Sim. O MapAtlas é hospedado na UE em data centers com certificação ISO 27001 e oferece um Contrato de Processamento de Dados (DPA) em conformidade com o GDPR, nos termos do Artigo 28. Isso atende aos requisitos típicos de compras do setor público. Manter o processamento dentro da UE também elimina as perguntas sobre transferências internacionais que as equipes de compras do setor público e da saúde fazem rotineiramente.

Achou útil? Compartilhe.

Sobre o autor

Brent van der Heiden

Escrito por

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. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

Ver todos os artigos →
Voltar ao blog