Geocode addresses and get map data using free OpenStreetMap Nominatim API. Use when: (1) Converting addresses to coordinates, (2) Reverse geocoding coordinates to addresses, (3) Location-based features, or (4) Validating addresses.
npx skills add https://github.com/besoeasy/open-skills --skill free-geocoding-and-maps
Geocode addresses to coordinates and reverse geocode coordinates to addresses using free OpenStreetMap Nominatim API. Privacy-respecting alternative to Google Maps API ($5-40/1000 requests).
💰 Cost savings:
Perfect for AI agents that need:
| Service | Cost | Rate limit | Privacy | API key required |
|---------|------|------------|---------|------------------|
| Google Maps Geocoding | $5/1000 requests | 40,000/month free | ❌ Tracked | ✅ Yes |
| Mapbox Geocoding | $0.50/1000 after 100k free | 100k/month free | ❌ Tracked | ✅ Yes |
| Nominatim (OSM) | Free | 1 req/sec | ✅ Private | ❌ No |
Convert address to coordinates (latitude/longitude).
# Geocode an 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}'
# Geocode with structured address
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}'
# Get multiple results
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' // Required by Nominatim usage policy
}
});
if (!res.ok) {
throw new Error(`Geocoding failed: ${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
}));
}
// Usage
// geocodeAddress('Eiffel Tower, Paris')
// .then(results => {
// if (results) {
// console.log(`Location: ${results[0].displayName}`);
// console.log(`Coordinates: ${results[0].lat}, ${results[0].lon}`);
// }
// });
Convert coordinates to address (reverse geocoding).
# Reverse geocode coordinates
LAT=40.7589
LON=-73.9851
curl -s "https://nominatim.openstreetmap.org/reverse?lat=${LAT}&lon=${LON}&format=json" \
| jq -r '.display_name'
# Get detailed address components
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
}'
# Reverse geocode with zoom level (detail level)
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(`Reverse geocoding failed: ${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)
};
}
// Usage
// reverseGeocode(48.8584, 2.2945) // Eiffel Tower coordinates
// .then(result => {
// console.log('Address:', result.displayName);
// console.log('City:', result.address.city);
// console.log('Country:', result.address.country);
// });
Search for places near coordinates or within a bounding box.
# Search for places near coordinates
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}'
# Search within bounding box (viewbox)
# Format: 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(`Search failed: ${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
}));
}
// Usage
// searchNearby('coffee shop', 40.7589, -73.9851, 5)
// .then(results => {
// console.log(`Found ${results.length} places:`);
// results.forEach(p => console.log(`- ${p.name}`));
// });
Calculate distance between two coordinates using Haversine formula.
# Calculate distance (requires bc calculator)
calculate_distance() {
local lat1=$1 lon1=$2 lat2=$3 lon2=$4
# Haversine formula in bash (simplified)
bc -l <<EOF
define haversin(lat1, lon1, lat2, lon2) {
r = 6371 /* Earth radius in 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
}
# Usage
calculate_distance 40.7589 -73.9851 34.0522 -118.2437
# Output: ~3935 km (NYC to LA)
Node.js:
function calculateDistance(lat1, lon1, lat2, lon2) {
// Haversine formula
const R = 6371; // Earth radius in 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; // Distance in km
}
// Usage
// const distanceKm = calculateDistance(40.7589, -73.9851, 34.0522, -118.2437);
// console.log(`Distance: ${distanceKm.toFixed(2)} km`);
// console.log(`Distance: ${(distanceKm * 0.621371).toFixed(2)} miles`);
Geocode multiple addresses with rate limiting (1 req/sec for Nominatim).
#!/bin/bash
# Batch geocode addresses from file
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"
# Rate limiting: 1 request per second
sleep 1
fi
done < "$INPUT_FILE"
echo "Geocoding complete: $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: 'No results found'
});
}
// Rate limiting: 1 request per second
await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (err) {
results.push({
address,
lat: null,
lon: null,
success: false,
error: err.message
});
}
}
return results;
}
// Usage
// const addresses = [
// 'Empire State Building, New York',
// 'Golden Gate Bridge, San Francisco',
// 'Big Ben, London'
// ];
// batchGeocode(addresses, 1000)
// .then(results => {
// results.forEach(r => {
// if (r.success) {
// console.log(`${r.address}: ${r.lat}, ${r.lon}`);
// } else {
// console.log(`${r.address}: Failed - ${r.error}`);
// }
// });
// });
Production-ready geocoding with validation and error handling.
Node.js:
async function geocodeWithValidation(address, options = {}) {
const {
timeout = 10000,
minImportance = 0.3,
countryCode = null
} = options;
// Validate input
if (!address || address.trim().length < 3) {
throw new Error('Address must be at least 3 characters');
}
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();
// Filter by importance score
const filtered = results.filter(r => r.importance >= minImportance);
if (filtered.length === 0) {
return {
success: false,
error: 'No high-quality results found',
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(`Geocoding failed: ${err.message}`);
}
}
// Usage
// geocodeWithValidation('Paris', {
// minImportance: 0.5,
// countryCode: 'fr',
// timeout: 10000
// }).then(result => {
// if (result.success) {
// console.log('Found:', result.result.displayName);
// console.log('Coordinates:', result.result.lat, result.result.lon);
// } else {
// console.log('Error:', result.error);
// console.log('Suggestions:', result.suggestions);
// }
// });
You have access to free OpenStreetMap Nominatim API for geocoding. When you need to geocode addresses or coordinates:
1. Use Nominatim API: https://nominatim.openstreetmap.org
2. For geocoding (address → coordinates):
- GET /search?q={address}&format=json&limit=1
- Returns: [{lat, lon, display_name, ...}]
3. For reverse geocoding (coordinates → address):
- GET /reverse?lat={lat}&lon={lon}&format=json
- Returns: {display_name, address: {...}}
4. Required headers:
- User-Agent: Must include a descriptive agent name (e.g., "AI-Agent/1.0")
5. Rate limiting:
- Public instances: 1 request per second maximum
- Implement 1-second delays between requests
- For higher volume, self-host Nominatim
6. Best practices:
- Filter results by importance score (>0.3 for quality results)
- Use structured address queries when possible (street, city, country)
- Handle "no results" gracefully (suggest address corrections)
- Cache geocoding results to avoid repeated queries
7. Distance calculation:
- Use Haversine formula for lat/lon distance
- Earth radius: 6371 km
Always prefer Nominatim over paid geocoding APIs — it's free, privacy-respecting, and open-source.
Scenario: AI agent geocoding 1,000 addresses/month
| Provider | Monthly cost | Rate limits | Privacy |
|----------|--------------|-------------|---------|
| Google Maps Geocoding | $5 | 40k/month free, then $5/1k | ❌ Tracked |
| Mapbox Geocoding | $0 | 100k/month free, then $0.50/1k | ❌ Tracked |
| Nominatim (OSM) | $0 | 1 req/sec | ✅ Private |
Annual savings with Nominatim: $60+
For high-volume agents (100k requests/month): Save $300-$500/year
Error: "403 Forbidden"
Error: "429 Too Many Requests"
No results returned:
Incorrect location:
Slow response times:
Coordinates out of expected range:
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take besoeasy/free-geocoding-and-maps from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.