Карта поиска поставщиков медицинских услуг относится к числу наиболее важных карт, которые может создать разработчик. Пациенты, использующие такую карту, часто испытывают тревогу, боль или неуверенность в незнакомой системе. Карта, которая загружается медленно, возвращает нерелевантные результаты или незаметно отправляет почтовый индекс пользователя стороннему сервису на базе США, означает не просто плохой опыт пользователя, а потенциальное нарушение 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, то есть данными о здоровье, поскольку Суд ЕС постановил, что данные, косвенно раскрывающие чувствительную информацию, подпадают под статью 9. Это верно даже если вы никогда не создаёте учётную запись пользователя и не храните профиль.
Риски вполне конкретные:
- Вызов API геокодирования на клиентской стороне, включающий введённый пользователем почтовый индекс, отправляется на сервер третьей стороны. Этот сервер видит почтовый индекс, искомую специальность и IP-адрес. Без DPA и гарантии размещения данных в ЕС это является проблемной передачей.
- Автодополнение браузера и история могут сохранить введённый почтовый индекс и запрос специалиста на устройстве пользователя, вне вашего контроля, но стоит отметить это в информации о конфиденциальности.
- Если вы логируете ответы API на стороне сервера (обычно для отладки), вы можете логировать комбинации почтовый индекс + специальность, не осознавая этого.
Решением является проксирование на бэкенде: ваш сервер получает поиск, вызывает API геокодирования MapAtlas и возвращает только координаты, никогда не отправляя исходный запрос на API третьей стороны из браузера пациента. Подробнее об этом в разделе реализации.
Структура данных поставщиков услуг
Данные поставщиков должны получаться из авторитетных официальных источников, а не из соскребленных наборов данных:
- UK: Directory of Healthcare Services (Service Search) API от NHS England и списки зарегистрированных поставщиков 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://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
});
// Маркеры поставщиков, зелёный = принимает, серый = полно
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://gateway.mapmetrics-atlas.net/forward-geocode/');
url.searchParams.set('text', req.query.q);
url.searchParams.set('token', 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&costing=auto`
);
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, 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 полигон
});
Статья Объяснение изохронных карт углубляется в варианты использования изохрон, включая изохроны общественного транспорта (полезно для пациентов без машины) и наложение нескольких временных зон, показывающее зоны 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 третьих сторон.
- Отсутствие логирования условий поиска в сеансе: если вы логируете запросы для отладки, удалите почтовый индекс перед записью в логи.
- Уведомление о конфиденциальности: уведомление о конфиденциальности вашего приложения должно раскрывать, что данные локации обрабатываются для поиска близлежащих поставщиков, законное основание по статье 6 и, если поисковые данные могут раскрыть сведения о здоровье, условие из статьи 9(2), например явное согласие, а также период хранения.
- Согласие на доступ к локации: если вы используете 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. Это удовлетворяет типичным требованиям закупок государственного сектора. Обработка внутри ЕС также снимает вопросы о международной передаче данных, которые закупочные команды государственного сектора и здравоохранения задают регулярно.
Часто задаваемые вопросы
Почему GDPR применяется к карте поиска врачей, даже если я не храню данные пациентов?
Место, по которому пациент ищет врача, показывает, где он живёт, и косвенно указывает на возможные заболевания (например, при поиске онкологов рядом с почтовым индексом). Согласно статье 9 GDPR, выводы о здоровье на основе данных о местоположении могут относиться к особой категории данных. Даже без хранения профилей запросы геокодирования в реальном времени, которые клиентская часть отправляет в сторонние API, создают факт обработки, и для него нужны правовое основание и раскрытие информации.
Что такое изохрона и чем она полезна для поиска врачей?
Изохрона представляет собой полигон, который охватывает все точки, достижимые за заданное время в пути от исходного места. На карте поиска врачей слой изохроны отвечает на вопрос «какие врачи находятся в 20 минутах езды?». Это гораздо полезнее простого расстояния, потому что учитываются дороги, ограничения скорости и характер трафика.
Можно ли использовать MapAtlas для приложения NHS или другого сервиса здравоохранения для пациентов?
Да. MapAtlas размещается в дата-центрах ЕС с сертификацией ISO 27001 и предоставляет соглашение об обработке данных по статье 28 GDPR. Это соответствует типичным требованиям государственных закупок. Обработка внутри ЕС также снимает вопросы о международной передаче данных, которые закупочные команды государственного сектора и здравоохранения задают регулярно.

