房产搜索是一个空间问题。买家和租户根据邻域、通勤时间和学校邻近度来思考,而不是邮编和街道列表。地图优先界面的转换效果优于列表优先界面,因为它让用户在按价格、卧室或任何其他属性进行筛选之前,在空间上锚定他们的搜索。
本教程构建了一个生产就绪的房产列表地图:低缩放级别的聚类标记、单个标记上的价格标签、包含列表详细信息的点击弹窗、重新中心化地图的邮编搜索以及实时筛选可见列表的价格范围滑块。完整的React组件不到100行。如果您不希望使用框架,相同的逻辑可在纯JavaScript中使用。
您还会找到针对EU PropTech平台特定的GDPR义务的说明,因为房产搜索生成的位置数据是个人数据,需要相应处理。
如果您是地图API的新手,How to Add Interactive Maps to Your Website教程在此之前介绍了基础知识。
房产数据结构
每个列表需要坐标、价格和弹窗足够的元数据。将其作为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编写的相同,使用MapAtlas瓷砖URL。
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样式特别适用于房产地图,较轻的底部使价格标签和彩色标记能够突出而不会造成视觉混乱。
具有价格标签的聚类
房产地图的关键UI模式是在单个标记上显示价格标签,并在聚类圆圈上显示计数气泡。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 = ''; });
价格范围过滤器
过滤可见列表的范围滑块是房产地图最高价值的UI补充之一。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);
请注意,当底层源更改时,聚类层会自动更新,从单个标记层中过滤出的属性也会从聚类计数中删除。
使用Geocoding API进行地址搜索
让用户输入邻域或邮编以重新中心化地图。Geocoding 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添加等时线覆盖。Isochrone Maps Explained文章展示了如何获取和显示该多边形。
The Complete React Component
Wrapping everything above into a React component with proper lifecycle management:
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>
);
}
That is the full component, under 100 lines including the price range slider and lifecycle cleanup.
GDPR Notes for EU PropTech
When a property seeker types their home address or clicks "use my location" on your map, they are sharing personal data under GDPR Article 4. That location data reveals where they live, which may infer other sensitive attributes about them.
Practical steps for EU compliance:
- Route geocoding requests through your backend rather than calling the Geocoding API directly from the browser. Your backend can strip identifying headers before forwarding.
- Do not log or persist search coordinates beyond the session unless you have a documented legal basis and have collected specific consent.
- If you implement "notify me when a property near me matches my criteria", treat the stored search area as personal data with a defined retention period.
The EU Developer's Guide to GDPR-Compliant Map APIs covers this in full, including processor agreements and data residency requirements. MapAtlas processes all data within the EU and provides the Data Processing Agreement required under GDPR Article 28.
The Real Estate industry solution page has additional context on how PropTech platforms use MapAtlas in production.
Next Steps
- Sign up for a free MapAtlas API key and start with the free tier
- Add travel-time overlays with the Travel Time API, show buyers what is reachable in 20 minutes
- Explore the Maps API styling guide to match your property platform's visual identity
Frequently Asked Questions
Can I use MapAtlas for a property listing website?
Yes. MapAtlas is used by PropTech platforms across the EU for property search maps, investment dashboards, and rental listing sites. The Maps API is Mapbox GL JS-compatible, so existing Mapbox-based property map code migrates with minimal changes.
How does marker clustering work for real estate maps?
Clustering groups nearby property markers into a single circle showing the count when the map is zoomed out. As the user zooms in, clusters split into individual markers. MapAtlas supports GeoJSON source clustering natively, set cluster: true on the source and the SDK handles the rest.
Is location data from property searchers subject to GDPR?
Yes. When a user searches by their home address or current location on a property site, that constitutes personal data under GDPR Article 4. EU PropTech platforms should route geocoding requests through a backend proxy and apply appropriate data minimisation and retention policies rather than making client-side API calls that log user locations to third-party servers.

