Skip to main content
Cómo construir un mapa de búsqueda de proveedores de salud
Tutorials

Cómo construir un mapa de búsqueda de proveedores de salud

Construye un mapa buscador de médicos conforme al RGPD con filtro de especialidad, búsqueda por código postal, isócronas de tiempo de viaje y privacidad desde el diseño.

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

Un buscador de proveedores sanitarios es uno de los mapas más relevantes que puede construir un desarrollador. Quienes lo usan suelen estar preocupados, con dolor o moviéndose por un sistema que no conocen. Un mapa que carga despacio, devuelve resultados irrelevantes o envía en silencio su código postal a un servicio de terceros con sede en Estados Unidos no es solo una mala experiencia de usuario: es una posible infracción del RGPD.

En este tutorial construyes un buscador de proveedores listo para producción: filtro por especialidad, búsqueda por código postal con la Geocoding API, una capa de isócronas que muestra "médicos a menos de 20 minutos" y un mapa funcional con la MapAtlas Maps API. El cumplimiento del RGPD forma parte de la arquitectura desde el principio, no se añade después.

La implementación completa está escrita en JavaScript puro, sin framework. Funciona en cualquier stack: React, Vue, HTML plano, WordPress o el CMS heredado de un hospital. El tutorial también cubre el contexto regulatorio europeo, los datos de la CQC en Reino Unido, la Kassenärztliche Vereinigung en Alemania y la CNAM en Francia, para que sepas de dónde obtener datos de proveedores de forma legítima.

Por qué las apps sanitarias de localización tienen obligaciones extra del RGPD

Las apps habituales recogen datos personales (nombre, email, uso). Las apps sanitarias añaden un segundo riesgo: deducir información de salud a partir de datos de ubicación.

Cuando un usuario busca "cardiólogos cerca de SE1 7PB", su código postal es un dato personal. La combinación de código postal y especialidad médica puede convertirse en una categoría especial de datos según el artículo 9(1) del RGPD, en concreto datos relativos a la salud, porque el Tribunal de Justicia ha establecido que los datos que revelan información sensible de forma indirecta entran en el ámbito del artículo 9. Esto se cumple aunque nunca crees una cuenta de usuario ni guardes un perfil.

Los riesgos son concretos:

  • Una llamada a una API de geocodificación desde el cliente que incluye el código postal escrito por el usuario llega a un servidor de terceros. Ese servidor ve el código postal, la especialidad buscada y la dirección IP. Sin un DPA y una garantía de residencia de datos en la UE, esa transferencia es problemática.
  • El autocompletado y el historial del navegador pueden conservar el código postal y la consulta de especialista en el dispositivo del usuario. Queda fuera de tu control, pero conviene mencionarlo en tu información de privacidad.
  • Si registras en el servidor las respuestas de la API (algo habitual para depurar), puedes estar guardando combinaciones de código postal y especialidad sin darte cuenta.

La solución es un proxy en el backend: tu servidor recibe la búsqueda, llama a la MapAtlas Geocoding API y devuelve solo las coordenadas. La consulta escrita nunca viaja desde el navegador del paciente a una API de terceros. Lo verás en detalle en la sección de implementación.

Estructura de los datos de proveedores

Los datos de proveedores deben venir de fuentes oficiales, no de conjuntos de datos obtenidos por scraping:

  • Reino Unido: la Directory of Healthcare Services (Service Search) API de NHS England y los listados de proveedores registrados en la CQC
  • Alemania: los datos abiertos de la Kassenärztliche Bundesvereinigung (KBV) y la API Arztsuche
  • Francia: la API Ameli de la CNAM y el répertoire RPPS

Estructura cada proveedor como 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"
      }
    }
  ]
};

Paso 1: inicializa el mapa con un filtro por especialidad

Empieza con el mapa y un desplegable de especialidades. El filtro actualiza los marcadores visibles en el cliente, sin ninguna petición de red.

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 proveedores: verde = acepta pacientes, gris = completo
  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 especialidad
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]);
  }
});

Paso 2: búsqueda por código postal a través de un proxy en el backend

En lugar de llamar a la Geocoding API desde el navegador (lo que expone el código postal del paciente a un servidor de terceros), envía la petición de geocodificación a través de un endpoint de tu propio backend. Tu servidor llama a MapAtlas y devuelve solo las coordenadas.

Frontend (navegador del paciente):

async function geocodePostcode(postcode) {
  // Llama a TU backend, no directamente a la API de geocodificación
  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 en el backend (Node.js/Express):

// /api/geocode, solo en el servidor: el código postal nunca sale de tu infraestructura
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); // clave en el 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;
  // Devuelve SOLO coordenadas: sin registrar el código postal ni incluir la especialidad
  res.json({ lat, lng });
});

Tu clave de API se queda en el servidor, el navegador del paciente nunca contacta directamente con el geocodificador y lo único que tu servidor transmite es el propio código postal, sin la especialidad ni nada que identifique al paciente.

Paso 3: capa de isócronas, "médicos a menos de 20 minutos"

Un polígono de isócrona muestra todos los puntos alcanzables dentro de un tiempo de conducción dado. Mostrarlo en un buscador de proveedores responde a la pregunta más práctica del paciente: "¿a cuántos médicos puedo llegar de verdad?"

async function fetchIsochrone(lat, lng) {
  // Llama a través de tu proxy en el backend por las mismas razones de privacidad
  const res = await fetch(
    `/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&costing=auto`
  );
  const geojson = await res.json();

  // Elimina la capa de isócrona anterior si existe
  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'); // Se inserta debajo de los marcadores para que los pines queden encima

  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ócronas en el 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
});

El artículo Mapas de isócronas explicados profundiza en los casos de uso de las isócronas, incluidas las de transporte público (útiles para pacientes sin coche) y las capas con varias franjas de tiempo que muestran a la vez zonas de 10, 20 y 30 minutos.

Paso 4: ventana emergente con el detalle del proveedor

Cuando un paciente hace clic en el marcador de un médico, muestra los datos esenciales: nombre, especialidad, si acepta pacientes, idiomas y próxima cita disponible.

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

Paso 5: panel de lista sincronizado con el mapa

Una vista de lista junto al mapa ayuda a quienes usan lectores de pantalla y a quienes prefieren desplazarse por una lista antes que hacer clic en los pines.

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

La lista se muestra en el orden en que llegan tus datos de proveedores. Para ordenarla por el tiempo real que se tarda en llegar a cada proveedor, haz que tu backend envíe el origen de la búsqueda y el conjunto filtrado de proveedores a la Distance Matrix API, y ordena las tarjetas según los tiempos de viaje que devuelve. Pasa la petición por el mismo proxy en el servidor que usas para geocodificar, por el motivo que explica la lista de comprobación de abajo, y se procesará dentro de la UE.

Lista de comprobación para implementar el RGPD

Si desarrollas para el sector sanitario en la UE, trata esto como una lista mínima de cumplimiento:

  • Proxy en el backend para geocodificar: los códigos postales de los pacientes no deben enviarse desde el cliente a APIs de terceros.
  • Sin registro de términos de búsqueda por sesión: si registras peticiones para depurar, elimina el código postal antes de escribir en los logs.
  • Aviso de privacidad: el aviso de privacidad de tu app debe indicar que se tratan datos de ubicación para encontrar proveedores cercanos, la base jurídica según el artículo 6 y, cuando los datos de búsqueda puedan revelar información de salud, una condición del artículo 9(2) como el consentimiento explícito, además del plazo de conservación.
  • Consentimiento para acceder a la ubicación: si usas la Geolocation API del navegador (navigator.geolocation.getCurrentPosition), muestra una explicación clara de la finalidad antes de lanzar la solicitud de permiso del navegador.
  • Residencia de datos: MapAtlas procesa todas las peticiones a la API dentro de la UE. Cita el DPA de MapAtlas al completar tu Registro de Actividades de Tratamiento.
  • Conservación: si guardas en caché en el servidor resultados de isócronas o coordenadas de búsqueda por rendimiento, define un plazo de conservación (por ejemplo, 24 horas) y automatiza el borrado.

La guía para desarrolladores europeos sobre APIs de mapas conformes con el RGPD cubre en detalle los contratos de encargado del tratamiento, la residencia de datos y los patrones de consentimiento. La página del sector de servicios sanitarios recoge las funciones de MapAtlas relevantes para aplicaciones de salud digital.

Qué construir a continuación

Preguntas frecuentes

¿Por qué se aplica el RGPD a un mapa buscador de médicos aunque no guarde datos de pacientes?

La ubicación que busca un paciente en un buscador sanitario revela dónde vive y, por deducción, qué afecciones puede tener (por ejemplo, al buscar oncólogos cerca de un código postal). Según el artículo 9 del RGPD, las deducciones relacionadas con la salud a partir de datos de ubicación pueden considerarse una categoría especial de datos. Aunque no guardes perfiles, las peticiones de geocodificación en tiempo real enviadas desde el cliente a APIs de terceros generan un tratamiento que necesita una base jurídica y debe comunicarse.

¿Qué es una isócrona y por qué es útil en un buscador de proveedores?

Una isócrona es un polígono que muestra todos los puntos alcanzables desde una ubicación de partida dentro de un tiempo de viaje dado. En un buscador sanitario, una capa de isócronas responde a "¿qué médicos están a menos de 20 minutos en coche?", una pregunta mucho más útil que la distancia en línea recta, porque tiene en cuenta las carreteras, los límites de velocidad y los patrones de tráfico.

¿Puedo usar MapAtlas en una aplicación del NHS o de salud pública orientada a pacientes?

Sí. MapAtlas está alojado en la UE en centros de datos con certificación ISO 27001 y ofrece un contrato de encargado del tratamiento conforme al RGPD según el artículo 28. Esto cubre los requisitos habituales de contratación del sector público. Mantener el tratamiento dentro de la UE también elimina las preguntas sobre transferencias internacionales que los equipos de contratación del sector público y sanitario plantean de forma habitual.

Preguntas frecuentes

¿Por qué se aplica el RGPD a un mapa buscador de médicos aunque no guarde datos de pacientes?

La ubicación que busca un paciente en un buscador sanitario revela dónde vive y, por deducción, qué afecciones puede tener (por ejemplo, al buscar oncólogos cerca de un código postal). Según el artículo 9 del RGPD, las deducciones relacionadas con la salud a partir de datos de ubicación pueden considerarse una categoría especial de datos. Aunque no guardes perfiles, las peticiones de geocodificación en tiempo real enviadas desde el cliente a APIs de terceros generan un tratamiento que necesita una base jurídica y debe comunicarse.

¿Qué es una isócrona y por qué es útil en un buscador de proveedores?

Una isócrona es un polígono que muestra todos los puntos alcanzables desde una ubicación de partida dentro de un tiempo de viaje dado. En un buscador sanitario, una capa de isócronas responde a «¿qué médicos están a menos de 20 minutos en coche?», una pregunta mucho más útil que la distancia en línea recta, porque tiene en cuenta las carreteras, los límites de velocidad y los patrones de tráfico.

¿Puedo usar MapAtlas en una aplicación del NHS o de salud pública orientada a pacientes?

Sí. MapAtlas está alojado en la UE en centros de datos con certificación ISO 27001 y ofrece un contrato de encargado del tratamiento conforme al RGPD según el artículo 28. Esto cubre los requisitos habituales de contratación del sector público. Mantener el tratamiento dentro de la UE también elimina las preguntas sobre transferencias internacionales que los equipos de contratación del sector público y sanitario plantean de forma habitual.

¿Le resultó útil? Compártalo.

Sobre el 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 los artículos →
Volver al blog