Поиск недвижимости - это пространственная задача. Покупатели и арендаторы думают о районах, времени в пути и близости к школам, а не о почтовых индексах и списках улиц. Интерфейс, ориентированный на карту, конвертирует лучше, чем интерфейс, ориентированный на список, потому что позволяет пользователям закрепить свой поиск пространственно перед фильтрацией по цене, спальням или любому другому атрибуту.
Это руководство создает готовую к производству карту листинга недвижимости: кластеризованные маркеры при низком масштабе, ценовые метки на отдельных маркерах, всплывающее окно при клике с деталями листинга, поиск по почтовому индексу, который переставляет карту, и ползунок ценового диапазона, который фильтрует видимые листинги в реальном времени. Полный компонент React занимает менее 100 строк. Та же логика работает в vanilla JavaScript, если вы предпочитаете отсутствие фреймворка.
Вы также найдете примечание об обязательствах GDPR, специфичных для платформ пропеча в ЕС, потому что данные о местоположении, генерируемые поиском недвижимости, являются персональными данными и должны обрабатываться соответствующим образом.
Если вы новичок в картографических API, учебное пособие Как добавить интерактивные карты на ваш сайт охватывает основы перед этим.
Структура данных недвижимости
Каждый листинг нуждается в координатах, цене и достаточных метаданных для всплывающего окна. Храните это как GeoJSON FeatureCollection, он подключается прямо к источнику карты без трансформации.
const properties = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
properties: {
id: "prop-001",
price: 485000,
bedrooms: 3,
sqm: 112,
address: "Keizersgracht 142, Amsterdam",
type: "apartment",
status: "for-sale"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
properties: {
id: "prop-002",
price: 1250,
bedrooms: 2,
sqm: 78,
address: "Sarphatistraat 58, Amsterdam",
type: "apartment",
status: "for-rent"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
properties: {
id: "prop-003",
price: 720000,
bedrooms: 4,
sqm: 195,
address: "Herengracht 380, Amsterdam",
type: "house",
status: "for-sale"
}
}
]
};
На реальной платформе этот массив поступает из вашего API листингов, вызова fetch при загрузке карты или изменении параметра запроса.
Настройка карты
Инициализируйте карту с центральным представлением вашего рынка недвижимости. MapAtlas Maps API совместим с Mapbox GL JS, поэтому вызов инициализации идентичен тому, что вы бы написали для Mapbox, с URL плитки MapAtlas.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/bright/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 13
});
Стиль Bright особенно хорошо работает для карт недвижимости, более легкая база позволяет ценовым меткам и цветным маркерам выделиться без визуального беспорядка.
Кластеризация с ценовыми метками
Ключевой паттерн интерфейса для карты недвижимости - это показание ценовых меток на отдельных маркерах и пузырей счета на кругах кластеров. Опция источника cluster: true автоматически группирует близлежащие точки. Вы добавляете отдельные слои для кластеров и отдельных маркеров.
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: properties,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Cluster circles, size scales with listing count
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'properties',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#2563EB',
'circle-radius': [
'step', ['get', 'point_count'],
22, 5, 30, 20, 38
],
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff'
}
});
// Cluster count labels
map.addLayer({
id: 'cluster-label',
type: 'symbol',
source: 'properties',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 14
},
paint: { 'text-color': '#ffffff' }
});
// Individual property markers, white circle with price text
map.addLayer({
id: 'property-price',
type: 'symbol',
source: 'properties',
filter: ['!', ['has', 'point_count']],
layout: {
'text-field': [
'concat',
'€',
['to-string', ['round', ['/', ['get', 'price'], 1000]]],
'k'
],
'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
'text-size': 12
},
paint: {
'text-color': '#1e293b',
'text-halo-color': '#ffffff',
'text-halo-width': 2
}
});
});
Взаимодействие при клике
Клик по кластеру приближает карту, чтобы показать отдельные листинги. Клик на отдельную недвижимость открывает детальное всплывающее окно.
// Zoom into clicked cluster
map.on('click', 'clusters', (e) => {
const [feature] = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
map.getSource('properties').getClusterExpansionZoom(
feature.properties.cluster_id,
(err, zoom) => {
if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom });
}
);
});
// Detail popup for individual listing
map.on('click', 'property-price', (e) => {
const { address, price, bedrooms, sqm, status, id } = e.features[0].properties;
const coords = e.features[0].geometry.coordinates.slice();
const formattedPrice = status === 'for-rent'
? `€${price.toLocaleString()}/mo`
: `€${price.toLocaleString()}`;
new mapmetricsgl.Popup({ offset: 10 })
.setLngLat(coords)
.setHTML(`
<div style="min-width:200px">
<strong style="font-size:15px">${formattedPrice}</strong>
<p style="margin:4px 0">${address}</p>
<p style="margin:4px 0;color:#64748b">${bedrooms} bed · ${sqm} m²</p>
<a href="/listings/${id}" style="color:#2563EB;font-weight:600">View listing →</a>
</div>
`)
.addTo(map);
});
// Change cursor on hover
map.on('mouseenter', 'property-price', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'property-price', () => { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', 'clusters', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'clusters', () => { map.getCanvas().style.cursor = ''; });
Фильтр диапазона цены
Ползунок диапазона, который фильтрует видимые листинги, является одной из наиболее ценных добавок интерфейса для карты недвижимости. Система выражений Mapbox GL JS позволяет вам обновить фильтр источника на стороне клиента без сетевого обхода, фильтрация происходит в браузере на уже загруженном GeoJSON.
function applyPriceFilter(min, max) {
const filter = ['all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], min],
['<=', ['get', 'price'], max]
];
map.setFilter('property-price', filter);
}
// Wire up range inputs
const minInput = document.getElementById('price-min');
const maxInput = document.getElementById('price-max');
function onRangeChange() {
applyPriceFilter(Number(minInput.value), Number(maxInput.value));
}
minInput.addEventListener('input', onRangeChange);
maxInput.addEventListener('input', onRangeChange);
Обратите внимание, что слой кластера обновляется автоматически при изменении базового источника, свойства, отфильтрованные из слоя отдельного маркера, также выпадают из числа кластеров.
Поиск адреса с помощью API геокодирования
Позвольте пользователям ввести район или почтовый индекс, чтобы переставить карту. API геокодирования возвращает функции GeoJSON, поэтому координаты попадают прямо в map.flyTo.
async function searchArea(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', 'YOUR_API_KEY');
url.searchParams.set('size', '1');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) return;
const [lng, lat] = data.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 13 });
}
document.getElementById('area-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') searchArea(e.target.value.trim());
});
Для поиска по времени в пути, "показать мне недвижимость в пределах 20 минут от этой школы", добавьте наложение изохроны с помощью MapAtlas Travel Time API. Статья Объяснение карт изохрон показывает, как получить и отобразить этот полигон.
Полный компонент React
Завернув все вышесказанное в компонент React с надлежащим управлением жизненным циклом:
import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
export function PropertyMap({ listings, apiKey }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
const [priceRange, setPriceRange] = useState([0, 2000000]);
useEffect(() => {
const map = new mapmetricsgl.Map({
container: containerRef.current,
style: `https://tiles.mapatlas.eu/styles/bright/style.json?key=${apiKey}`,
center: [4.9041, 52.3676],
zoom: 13
});
mapRef.current = map;
map.on('load', () => {
map.addSource('properties', {
type: 'geojson',
data: { type: 'FeatureCollection', features: listings },
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 60
});
// Add cluster, cluster-label, property-price layers (see above)
// Add click handlers (see above)
});
return () => map.remove();
}, [apiKey]);
// Update filter when price range slider changes
useEffect(() => {
const map = mapRef.current;
if (!map || !map.getLayer('property-price')) return;
map.setFilter('property-price', [
'all',
['!', ['has', 'point_count']],
['>=', ['get', 'price'], priceRange[0]],
['<=', ['get', 'price'], priceRange[1]]
]);
}, [priceRange]);
return (
<div>
<div style={{ padding: '12px 0', display: 'flex', gap: 12 }}>
<label>
Min €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[0]}
onChange={e => setPriceRange([+e.target.value, priceRange[1]])} />
</label>
<label>
Max €
<input type="range" min={0} max={2000000} step={10000}
value={priceRange[1]}
onChange={e => setPriceRange([priceRange[0], +e.target.value])} />
</label>
</div>
<div ref={containerRef} style={{ width: '100%', height: '600px' }} />
</div>
);
}
Это полный компонент, менее 100 строк, включая ползунок ценового диапазона и очистку жизненного цикла.
Примечания GDPR для пропеча ЕС
Когда соискатель недвижимости вводит свой домашний адрес или нажимает "использовать мое местоположение" на вашей карте, он делится личными данными в соответствии со статьей 4 GDPR. Эти данные о местоположении раскрывают, где он живет, что может подразумевать другие конфиденциальные характеристики о нем.
Практические шаги для соответствия требованиям ЕС:
- Маршрутизируйте запросы геокодирования через ваш бэкенд, а не вызывайте API геокодирования прямо из браузера. Ваш бэкенд может удалить идентифицирующие заголовки перед переадресацией.
- Не регистрируйте и не сохраняйте координаты поиска за пределами сеанса, если у вас нет задокументированного юридического основания и вы не получили конкретное согласие.
- Если вы реализуете "уведомлять меня, когда свойство рядом со мной соответствует моим критериям", рассматривайте сохраненную область поиска как персональные данные с определенным периодом хранения.
Руководство разработчика ЕС по API картографии, совместимым с GDPR охватывает это полностью, включая соглашения обработчика и требования остаточности данных. MapAtlas обрабатывает все данные в ЕС и предоставляет соглашение об обработке данных, требуемое в соответствии со статьей 28 GDPR.
Страница решения для промышленности недвижимости имеет дополнительный контекст о том, как платформы пропеча используют MapAtlas в производстве.
Следующие шаги
- Зарегистрируйтесь для получения бесплатного ключа API MapAtlas и начните с бесплатного уровня
- Добавьте наложения времени в пути с помощью Travel Time API, показывают покупателям, что доступно в пределах 20 минут
- Изучите руководство по стилю Maps API чтобы соответствовать визуальной идентичности вашей платформы недвижимости
Часто задаваемые вопросы
Могу ли я использовать MapAtlas для веб-сайта объявлений о недвижимости?
Да. MapAtlas используется платформами пропеча по всему ЕС для карт поиска недвижимости, инвестиционных панелей и сайтов объявлений об аренде. Maps API совместим с Mapbox GL JS, поэтому существующий код карты на основе Mapbox мигрирует с минимальными изменениями.
Как работает кластеризация маркеров для карт недвижимости?
Кластеризация группирует близлежащие маркеры недвижимости в один круг, показывающий количество, когда карта приближена. Когда пользователь приближается, кластеры разбиваются на отдельные маркеры. MapAtlas поддерживает кластеризацию источника GeoJSON изначально, установите cluster: true на источнике и SDK справляется с остальным.
Подпадают ли данные о местоположении от соискателей недвижимости под GDPR?
Да. Когда пользователь выполняет поиск по своему домашнему адресу или текущему местоположению на сайте недвижимости, это является личными данными в соответствии со статьей 4 GDPR. Платформы пропеча в ЕС должны маршрутизировать запросы геокодирования через прокси-сервер бэкенда и применять надлежащую политику минимизации и хранения данных, а не делать клиентские вызовы API, которые регистрируют местоположения пользователей на серверах третьих лиц.

