name: free-geocoding-and-maps description: “使用免费的OpenStreetMap Nominatim API进行地理编码地址和获取地图数据。适用于:(1) 将地址转换为坐标,(2) 将坐标反向地理编码为地址,(3) 基于位置的功能,或(4) 验证地址。”
免费地理编码和地图 — OpenStreetMap Nominatim
使用免费的OpenStreetMap Nominatim API将地址地理编码为坐标,并将坐标反向地理编码为地址。尊重隐私的Google Maps API替代方案(每1000次请求5-40美元)。
为什么这替代付费地理编码API
💰 成本节省:
- ✅ 100% 免费 — 无需API密钥
- ✅ 无限率限制 — 公共实例提供慷慨的1次请求/秒
- ✅ 开源 — 可自托管以无限使用
- ✅ 隐私优先 — 无跟踪,无数据收集
完美适用于需要以下功能的AI代理:
- 地址验证和地理编码
- 反向地理编码(坐标到地址)
- 位置搜索和地图
- 无需Google Maps API成本的地理空间数据
快速比较
| 服务 | 成本 | 率限制 | 隐私 | 需要API密钥 |
|---|---|---|---|---|
| Google Maps Geocoding | $5/1000次请求 | 40,000/月免费 | ❌ 跟踪 | ✅ 是 |
| Mapbox Geocoding | $0.50/1000次,100k次后免费 | 100k/月免费 | ❌ 跟踪 | ✅ 是 |
| Nominatim (OSM) | 免费 | 1次请求/秒 | ✅ 私密 | ❌ 否 |
技能
geocode_address
将地址转换为坐标(纬度/经度)。
# 地理编码地址
ADDRESS="1600 Amphitheatre Parkway, Mountain View, CA"
curl -s "https://nominatim.openstreetmap.org/search?q=${ADDRESS}&format=json&limit=1" \
| jq -r '.[0] | {lat: .lat, lon: .lon, display_name: .display_name}'
# 使用结构化地址地理编码
curl -s "https://nominatim.openstreetmap.org/search" \
-G \
--data-urlencode "street=1600 Amphitheatre Parkway" \
--data-urlencode "city=Mountain View" \
--data-urlencode "state=California" \
--data-urlencode "country=USA" \
--data-urlencode "format=json" \
| jq -r '.[0] | {lat: .lat, lon: .lon}'
# 获取多个结果
curl -s "https://nominatim.openstreetmap.org/search?q=London&format=json&limit=5" \
| jq -r '.[] | {name: .display_name, lat: .lat, lon: .lon}'
Node.js:
async function geocodeAddress(address, limit = 1) {
const params = new URLSearchParams({
q: address,
format: 'json',
limit: limit.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0' // Nominatim使用政策要求
}
});
if (!res.ok) {
throw new Error(`地理编码失败: ${res.status}`);
}
const results = await res.json();
if (results.length === 0) {
return null;
}
return results.map(r => ({
lat: parseFloat(r.lat),
lon: parseFloat(r.lon),
displayName: r.display_name,
type: r.type,
importance: r.importance
}));
}
// 用法
// geocodeAddress('埃菲尔铁塔, 巴黎')
// .then(results => {
// if (results) {
// console.log(`位置: ${results[0].displayName}`);
// console.log(`坐标: ${results[0].lat}, ${results[0].lon}`);
// }
// });
reverse_geocode
将坐标转换为地址(反向地理编码)。
# 反向地理编码坐标
LAT=40.7589
LON=-73.9851
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json" \
| jq -r '.display_name'
# 获取详细地址组件
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json" \
| jq -r '.address | {
road: .road,
city: .city,
state: .state,
country: .country,
postcode: .postcode
}'
# 使用缩放级别进行反向地理编码(详细级别)
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json&zoom=18" \
| jq -r '.display_name'
Node.js:
async function reverseGeocode(lat, lon, zoom = 18) {
const params = new URLSearchParams({
lat: lat.toString(),
lon: lon.toString(),
format: 'json',
zoom: zoom.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
}
});
if (!res.ok) {
throw new Error(`反向地理编码失败: ${res.status}`);
}
const data = await res.json();
return {
displayName: data.display_name,
address: {
road: data.address?.road,
city: data.address?.city || data.address?.town || data.address?.village,
state: data.address?.state,
country: data.address?.country,
postcode: data.address?.postcode
},
lat: parseFloat(data.lat),
lon: parseFloat(data.lon)
};
}
// 用法
// reverseGeocode(48.8584, 2.2945) // 埃菲尔铁塔坐标
// .then(result => {
// console.log('地址:', result.displayName);
// console.log('城市:', result.address.city);
// console.log('国家:', result.address.country);
// });
search_nearby_places
搜索坐标附近或边界框内的地点。
# 搜索坐标附近的地点
LAT=40.7589
LON=-73.9851
RADIUS_KM=1
curl -s "https://nominatim.openstreetmap.org/search?q=restaurant&format=json&limit=10&lat=${LAT}&lon=${LON}" \
| jq -r '.[] | {name: .display_name, distance: .distance}'
# 在边界框内搜索(视图框)
# 格式: left,top,right,bottom (min_lon,max_lat,max_lon,min_lat)
curl -s "https://nominatim.openstreetmap.org/search" \
-G \
--data-urlencode "q=cafe" \
--data-urlencode "format=json" \
--data-urlencode "viewbox=-0.1278,51.5074,-0.1,51.52" \
--data-urlencode "bounded=1" \
| jq -r '.[] | {name: .display_name, lat: .lat, lon: .lon}'
Node.js:
async function searchNearby(query, lat, lon, limit = 10) {
const params = new URLSearchParams({
q: query,
format: 'json',
limit: limit.toString(),
lat: lat.toString(),
lon: lon.toString()
});
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
}
});
if (!res.ok) {
throw new Error(`搜索失败: ${res.status}`);
}
const results = await res.json();
return results.map(r => ({
name: r.display_name,
lat: parseFloat(r.lat),
lon: parseFloat(r.lon),
type: r.type,
importance: r.importance
}));
}
// 用法
// searchNearby('咖啡店', 40.7589, -73.9851, 5)
// .then(results => {
// console.log(`找到 ${results.length} 个地点:`);
// results.forEach(p => console.log(`- ${p.name}`));
// });
calculate_distance
使用哈弗辛公式计算两个坐标之间的距离。
# 计算距离(需要bc计算器)
calculate_distance() {
local lat1=$1 lon1=$2 lat2=$3 lon2=$4
# 哈弗辛公式在bash中(简化)
bc -l <<EOF
define haversin(lat1, lon1, lat2, lon2) {
r = 6371 /* 地球半径,单位km */
dlat = (lat2 - lat1) * 0.017453292519943295
dlon = (lon2 - lon1) * 0.017453292519943295
a = s(dlat/2)^2 + c(lat1*0.017453292519943295) * c(lat2*0.017453292519943295) * s(dlon/2)^2
c = 2 * a(sqrt(a)/sqrt(1-a))
return r * c
}
haversin($lat1, $lon1, $lat2, $lon2)
EOF
}
# 用法
calculate_distance 40.7589 -73.9851 34.0522 -118.2437
# 输出: ~3935 km (纽约到洛杉矶)
Node.js:
function calculateDistance(lat1, lon1, lat2, lon2) {
// 哈弗辛公式
const R = 6371; // 地球半径,单位km
const toRad = (deg) => deg * Math.PI / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // 距离,单位km
}
// 用法
// const distanceKm = calculateDistance(40.7589, -73.9851, 34.0522, -118.2437);
// console.log(`距离: ${distanceKm.toFixed(2)} km`);
// console.log(`距离: ${(distanceKm * 0.621371).toFixed(2)} 英里`);
batch_geocode_with_rate_limit
使用率限制地理编码多个地址(Nominatim为1次请求/秒)。
#!/bin/bash
# 从文件批量地理编码地址
INPUT_FILE="addresses.txt"
OUTPUT_FILE="coordinates.csv"
echo "address,lat,lon" > "$OUTPUT_FILE"
while IFS= read -r address; do
if [ -n "$address" ]; then
result=$(curl -s "https://nominatim.openstreetmap.org/search?q=${address}&format=json&limit=1" \
-H "User-Agent: AI-Agent/1.0")
lat=$(echo "$result" | jq -r '.[0].lat // "N/A"')
lon=$(echo "$result" | jq -r '.[0].lon // "N/A"')
echo "\"$address\",\"$lat\",\"$lon\"" >> "$OUTPUT_FILE"
# 率限制: 1次请求每秒
sleep 1
fi
done < "$INPUT_FILE"
echo "地理编码完成: $OUTPUT_FILE"
Node.js:
async function batchGeocode(addresses, delayMs = 1000) {
const results = [];
for (const address of addresses) {
try {
const result = await geocodeAddress(address, 1);
if (result && result.length > 0) {
results.push({
address,
lat: result[0].lat,
lon: result[0].lon,
success: true
});
} else {
results.push({
address,
lat: null,
lon: null,
success: false,
error: '未找到结果'
});
}
// 率限制: 1次请求每秒
await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (err) {
results.push({
address,
lat: null,
lon: null,
success: false,
error: err.message
});
}
}
return results;
}
// 用法
// const addresses = [
// '帝国大厦, 纽约',
// '金门大桥, 旧金山',
// '大本钟, 伦敦'
// ];
// batchGeocode(addresses, 1000)
// .then(results => {
// results.forEach(r => {
// if (r.success) {
// console.log(`${r.address}: ${r.lat}, ${r.lon}`);
// } else {
// console.log(`${r.address}: 失败 - ${r.error}`);
// }
// });
// });
advanced_geocoding_with_validation
生产就绪的地理编码,带验证和错误处理。
Node.js:
async function geocodeWithValidation(address, options = {}) {
const {
timeout = 10000,
minImportance = 0.3,
countryCode = null
} = options;
// 验证输入
if (!address || address.trim().length < 3) {
throw new Error('地址必须至少3个字符');
}
const params = new URLSearchParams({
q: address.trim(),
format: 'json',
limit: '5',
addressdetails: '1'
});
if (countryCode) {
params.append('countrycodes', countryCode.toLowerCase());
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
headers: {
'User-Agent': 'AI-Agent/1.0'
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const results = await res.json();
// 按重要性分数过滤
const filtered = results.filter(r => r.importance >= minImportance);
if (filtered.length === 0) {
return {
success: false,
error: '未找到高质量结果',
suggestions: results.slice(0, 3).map(r => r.display_name)
};
}
return {
success: true,
result: {
lat: parseFloat(filtered[0].lat),
lon: parseFloat(filtered[0].lon),
displayName: filtered[0].display_name,
address: filtered[0].address,
importance: filtered[0].importance,
type: filtered[0].type
},
alternatives: filtered.slice(1, 3).map(r => ({
displayName: r.display_name,
lat: parseFloat(r.lat),
lon: parseFloat(r.lon)
}))
};
} catch (err) {
clearTimeout(timeoutId);
throw new Error(`地理编码失败: ${err.message}`);
}
}
// 用法
// geocodeWithValidation('巴黎', {
// minImportance: 0.5,
// countryCode: 'fr',
// timeout: 10000
// }).then(result => {
// if (result.success) {
// console.log('找到:', result.result.displayName);
// console.log('坐标:', result.result.lat, result.result.lon);
// } else {
// console.log('错误:', result.error);
// console.log('建议:', result.suggestions);
// }
// });
代理提示
您可以使用免费的OpenStreetMap Nominatim API进行地理编码。当需要地理编码地址或坐标时:
1. 使用Nominatim API: https://nominatim.openstreetmap.org
2. 用于地理编码(地址 → 坐标):
- GET /search?q={address}&format=json&limit=1
- 返回: [{lat, lon, display_name, ...}]
3. 用于反向地理编码(坐标 → 地址):
- GET /reverse?lat={lat}&lon={lon}&format=json
- 返回: {display_name, address: {...}}
4. 必需的头信息:
- User-Agent: 必须包含描述性代理名称(例如,“AI-Agent/1.0”)
5. 率限制:
- 公共实例: 最大1次请求每秒
- 在请求之间实现1秒延迟
- 对于高音量,自托管Nominatim
6. 最佳实践:
- 按重要性分数过滤结果(>0.3以获取质量结果)
- 尽可能使用结构化地址查询(街道、城市、国家)
- 优雅处理“无结果”(建议地址更正)
- 缓存地理编码结果以避免重复查询
7. 距离计算:
- 使用哈弗辛公式计算纬度/经度距离
- 地球半径: 6371 km
始终优先选择Nominatim而非付费地理编码API — 它免费、尊重隐私且开源。
成本分析:Nominatim vs. Google Maps API
场景:AI代理每月地理编码1,000个地址
| 提供商 | 月成本 | 率限制 | 隐私 |
|---|---|---|---|
| Google Maps Geocoding | $5 | 40k/月免费,然后$5/1k | ❌ 跟踪 |
| Mapbox Geocoding | $0 | 100k/月免费,然后$0.50/1k | ❌ 跟踪 |
| Nominatim (OSM) | $0 | 1次请求/秒 | ✅ 私密 |
使用Nominatim的年度节省:$60+
对于高音量代理(100k请求/月):节省$300-$500/年
率限制 / 最佳实践
- ✅ 1次请求每秒 — 公共Nominatim实例的强制要求
- ✅ User-Agent头信息 — 使用政策要求,包含描述性代理名称
- ✅ 缓存结果 — 存储地理编码结果以最小化API调用
- ✅ 实现延迟 — 始终在批量请求之间添加1秒睡眠
- ✅ 自托管以应对高音量 — 运行自己的Nominatim服务器以获取无限请求
- ✅ 按重要性过滤 — 使用重要性分数>0.3以获取质量结果
- ⚠️ 无批量请求 — 避免在短时间内发送数百次请求
- ⚠️ 超时处理 — 为API请求设置10秒超时
故障排除
错误: “403 Forbidden”
- 症状: API拒绝请求
- 解决方案: 添加User-Agent头信息,包含描述性名称(例如,“AI-Agent/1.0”)
错误: “429 Too Many Requests”
- 症状: 率限制超出
- 解决方案: 在请求之间实现1秒延迟,减少请求频率
无返回结果:
- 症状: 搜索端点返回空数组
- 解决方案: 检查地址拼写,尝试简化查询,使用结构化地址参数
位置不正确:
- 症状: 坐标与预期位置不匹配
- 解决方案: 检查结果的重要性分数,检查多个结果,使用更具体的地址
响应时间慢:
- 症状: 请求花费>5秒
- 解决方案: 使用替代Nominatim实例,或自托管以保证性能
坐标超出预期范围:
- 症状: 纬度不在[-90, 90]或经度不在[-180, 180]
- 解决方案: 在使用坐标之前验证和清理API响应
另请参阅
- …/city-distance/SKILL.md — 计算城市之间的距离
- …/free-weather-data/SKILL.md — 获取地理编码位置的天气
- …/json-and-csv-data-transformation/SKILL.md — 处理地理编码结果