Карта поиска поставщиков медицинских услуг - одна из наиболее важных карт, которую может создать разработчик. Пациенты, использующие такую карту, часто испытывают тревогу, боль или неуверенность в незнакомой системе. Карта, которая загружается медленно, возвращает нерелевантные результаты или незаметно отправляет почтовый индекс пользователя стороннему сервису на базе США - это не просто плохой опыт пользователя, это потенциальное нарушение GDPR.
Этот туториал создаёт готовую к производству карту поиска врачей: фильтр по специальности, поиск по почтовому индексу с помощью API геокодирования, наложение изохроны, показывающее "врачи в пределах 20 минут", и рабочую карту, используя MapAtlas Maps API, всё это с соблюдением GDPR, встроенным в архитектуру с самого начала, а не добавленным постфактум.
Полная реализация написана на ванильном JavaScript без необходимости фреймворка. Это работает на любом стеке: React, Vue, обычный HTML, WordPress или устаревшей CMS больницы. Туториал также охватывает нормативно-правовой контекст ЕС, данные UK CQC, немецкую Kassenärztliche Vereinigung и французскую CNAM, так что вы будете знать, откуда легально получить данные врачей.
Почему приложения здравоохранения с локацией имеют дополнительные обязательства GDPR
Стандартные приложения собирают персональные данные (имя, email, использование). Приложения здравоохранения имеют второй риск: вывод информации о здоровье из данных локации.
Когда пользователь ищет "кардиологи рядом с SE1 7PB", его почтовый индекс - это персональные данные. Комбинация почтового индекса и медицинской специальности становится потенциально данными особой категории согласно статье 9(1) GDPR, данными, раскрывающими состояние здоровья. Это верно даже если вы никогда не создаёте учётную запись пользователя и не храните профиль.
Риски вполне конкретные:
- Вызов API геокодирования на клиентской стороне, включающий введённый пользователем почтовый индекс, отправляется на сервер третьей стороны. Этот сервер видит почтовый индекс, искомую специальность и IP-адрес. Без DPA и гарантии размещения данных в ЕС это является проблемной передачей.
- Автодополнение браузера и история могут сохранить введённый почтовый индекс и запрос специалиста на устройстве пользователя, вне вашего контроля, но стоит отметить это в информации о конфиденциальности.
- Если вы логируете ответы API на стороне сервера (обычно для отладки), вы можете логировать комбинации почтовый индекс + специальность, не осознавая этого.
Решением является проксирование на бэкенде: ваш сервер получает поиск, вызывает API геокодирования MapAtlas и возвращает только координаты, никогда не отправляя исходный запрос на API третьей стороны из браузера пациента. Подробнее об этом в разделе реализации.
Структура данных поставщиков услуг
Данные поставщиков должны получаться из авторитетных официальных источников, а не из соскребленных наборов данных:
- UK: NHS Choices API NHS Digital и списки зарегистрированных поставщиков CQC
- Germany: открытые данные Kassenärztliche Bundesvereinigung (KBV) и API Arztsuche
- France: API Ameli CNAM и répertoire RPPS
Структурируйте каждого поставщика как объект 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"
}
}
]
};
Шаг 1: Инициализация карты с фильтром по специальности
Начните с карты и раскрывающегося списка специальности. Фильтр обновляет видимые маркеры на клиентской стороне без сетевого запроса.
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
});
// Маркеры поставщиков, зелёный = принимает, серый = полно
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);
});
// Фильтр по специальности
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]);
}
});
Шаг 2: Поиск по почтовому индексу через бэкенд прокси
Вместо вызова API геокодирования из браузера (что раскрывает почтовый индекс пациента стороннему серверу), маршрутизируйте запрос геокодирования через собственную конечную точку бэкенда. Ваш сервер вызывает MapAtlas и возвращает только координаты.
Фронтенд (браузер пациента):
async function geocodePostcode(postcode) {
// Вызываем ВАШ бэкенд, а не 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);
});
Прокси бэкенда (Node.js/Express):
// /api/geocode, только на стороне сервера, почтовый индекс никогда не покидает вашу инфраструктуру
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); // ключ на стороне сервера
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;
// Возвращаем ТОЛЬКО координаты, почтовый индекс не логируется, специальность не в ответе
res.json({ lat, lng });
});
Ваш API ключ остаётся на сервере и исходный запрос пациента никогда не отправляется третьей стороне.
Шаг 3: Наложение изохроны, "Врачи в пределах 20 минут"
Полигон изохроны показывает каждую точку, достижимую в пределах данного времени в пути. Отображение её на карте поиска врачей отвечает на наиболее практический вопрос пациента: "сколько врачей я действительно могу достичь?"
async function fetchIsochrone(lat, lng) {
// Вызовите через ваш бэкенд прокси по тем же причинам конфиденциальности
const res = await fetch(
`/api/isochrone?lat=${lat}&lng=${lng}&minutes=20&profile=driving`
);
const geojson = await res.json();
// Удалите существующий слой изохроны если он есть
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'); // Вставьте ниже маркеров, чтобы булавки отображались сверху
map.addLayer({
id: 'isochrone-border',
type: 'line',
source: 'isochrone',
paint: {
'line-color': '#16A34A',
'line-width': 2,
'line-dasharray': [3, 2]
}
}, 'provider-markers');
}
Прокси бэкенда для изохроны:
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); // секунды
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 полигон
});
Статья Объяснение изохронных карт углубляется в варианты использования изохрон, включая изохроны общественного транспорта (полезно для пациентов без машины) и наложение нескольких временных зон, показывающее зоны 10, 20 и 30 минут одновременно.
Шаг 4: Всплывающее окно деталей поставщика
Когда пациент кликает на маркер врача, покажите необходимые детали: имя, специальность, статус принятия пациентов, языки и время следующей доступной консультации.
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">Принимает пациентов</span>'
: '<span style="color:#9CA3AF">Не принимает новых пациентов</span>';
const apptLine = accepting && nextAvailable
? `<p style="margin:4px 0">Доступно: <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">
Языки: ${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 = ''; });
Шаг 5: Панель списка синхронизирована с картой
Представление в виде списка рядом с картой помогает пользователям с экранными дикторами и тем, кто предпочитает прокручивать вместо нажатия на булавки.
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 ? `Доступно: ${nextAvailable || 'позвонить для подтверждения'}` : 'Не принимает'}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 15 });
});
list.appendChild(card);
});
}
Контрольный список реализации GDPR
Для разработчиков здравоохранения ЕС рассматривайте это как минимальный контрольный список соответствия:
- Прокси бэкенда для геокодирования: почтовые индексы пациентов не должны отправляться на клиентской стороне на API третьих сторон.
- Отсутствие логирования условий поиска в сеансе: если вы логируете запросы для отладки, удалите почтовый индекс перед записью в логи.
- Уведомление о конфиденциальности: уведомление о конфиденциальности вашего приложения должно раскрывать, что данные локации обрабатываются для поиска близлежащих поставщиков, законное основание (законный интерес или явное согласие) и период хранения.
- Согласие на доступ к локации: если вы используете API Geolocation браузера (
navigator.geolocation.getCurrentPosition), покажите чёткое объяснение цели перед срабатыванием приглашения разрешения браузера. - Размещение данных: MapAtlas обрабатывает все запросы API в ЕС. Ссылайтесь на DPA MapAtlas при заполнении реестра деятельности обработки.
- Удержание: если вы кэшируете результаты изохрон или координаты поиска на стороне сервера для производительности, определите период удержания (например, 24 часа) и автоматизируйте удаление.
Руководство разработчика ЕС по API карт, соответствующим GDPR охватывает соглашения обработчика, размещение данных и паттерны согласия полностью. Страница индустрии услуг здравоохранения перечисляет конкретные функции MapAtlas, релевантные приложениям цифрового здравоохранения.
Что создать далее
- Зарегистрируйтесь для бесплатного ключа MapAtlas API, один ключ для карт, геокодирования, маршрутизации и изохрон
- Добавьте паттерн туториала поиска магазинов для многоклинических сетей более чем с несколькими местоположениями
- Изучите страницу возможностей API геокодирования для поиска по почтовому индексу, обратного геокодирования и пакетной валидации адресов поставщиков
Часто задаваемые вопросы
Почему GDPR применяется к карте поиска врачей даже если я не храню данные пациентов?
Местоположение поиска пациента на карте поиска здравоохранения раскрывает, где они живут и, по выводам, какие условия они могут иметь (например, поиск онкологов рядом с почтовым индексом). Согласно статье 9 GDPR, выводы о здоровье из данных локации могут квалифицироваться как данные особой категории. Даже без хранения профилей, запросы геокодирования в реальном времени, отправленные на клиентской стороне на API третьих сторон, создают запись обработки, требующую законного основания и раскрытия.
Что такое изохрона и почему она полезна для поиска поставщиков?
Изохрона - это полигон, который показывает каждую точку, достижимую в пределах данного времени в пути из начальной точки. На карте поиска врачей наложение изохроны отвечает на "какие врачи в пределах 20 минут на машине?", гораздо более полезный вопрос, чем прямое расстояние, потому что это учитывает дороги, ограничения скорости и паттерны трафика.
Могу ли я использовать MapAtlas для приложения здравоохранения, обращённого к пациентам NHS или органам здравоохранения?
Да. MapAtlas размещена в ЕС, сертифицирована ISO 27001 и предоставляет согласованное с GDPR соглашение об обработке данных в соответствии со статьей 28. Это удовлетворяет типичным требованиям закупок государственного сектора. UK NHS Digital и эквивалентные органы в Германии (DSGVO) и Франции (CNIL) требуют размещения данных в ЕС/UK, что MapAtlas обеспечивает.

