Places API
Preview — This API is unstable and will change. Do not depend on the current request/response shape in production integrations.
The Places API provides hybrid text and geographic search across the Geog places database. Built on a geo-composite cell file architecture using H3 cell partitioning for efficient spatial queries.
Base URL
https://api.geog.dev/v1/places
Authentication
Places API requests require a Bearer token with places:read scope. For frontend applications, exchange a long-lived API token for a short-lived access token to avoid exposing credentials in client-side code:
# 1. Exchange your API token for a short-lived access token (run on your backend)
curl -X POST "https://api.geog.dev/v1/auth/token" \
-H "Authorization: Bearer your-long-lived-api-token" \
-H "Content-Type: application/json" \
-d '{"ttl": 3600, "scope": "places:read"}'
# Response: { "access_token": "eyJhbGc...", "expires_in": 3600, ... }
# 2. Use the short-lived token for places requests
curl -X POST "https://api.geog.dev/v1/places/search" \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"location": {"lat": 44.9778, "lon": -93.2650}, "radius": 5000}'
See Token Exchange for the full endpoint specification.
Endpoints
1. Search Places
Search for places using text queries, geographic constraints, and category filters.
/v1/places/searchRequest Body
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
location | object | Yes | — | Geographic center point: { "lat": number, "lon": number } |
radius | number | No | 5000 | Search radius in meters (1–100,000) |
query | string | No | — | Text search query (e.g., "coffee shops") |
categories | string[] | No | — | Filter by categories: ["restaurant", "cafe"] |
limit | integer | No | 20 | Results per page (1–1,000) |
offset | integer | No | 0 | Pagination offset |
weights | object | No | — | Hybrid scoring weights: { "text": 0.6, "geo": 0.4 } |
Location constraints:
lat: Latitude (-90 to 90)lon: Longitude (-180 to 180)
Weights object:
text: Weight for text relevance scoring (0–1)geo: Weight for geographic proximity scoring (0–1)
Example Request
curl -X POST "https://api.geog.dev/v1/places/search" \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{
"query": "museum",
"location": { "lat": 44.9778, "lon": -93.2650 },
"radius": 5000,
"limit": 5
}'
Example Response
{
"results": [
{
"id": "8527526bfffffff_w157035918",
"name": "Mill City Museum",
"categories": ["museum", "arts_and_entertainment"],
"location": { "lat": 44.9790262, "lon": -93.2568813 },
"address": {
"housenumber": "704",
"street": "South 2nd Street",
"city": "Minneapolis",
"state": "MN",
"postcode": "55401"
},
"contact": {
"email": "mcm@mnhs.org",
"phone": "+1 612 341 7555",
"website": "https://www.mnhs.org/millcity"
},
"hours": "Th,Fr 10:00-16:00; Sa,Su 10:00-17:00",
"access": { "wheelchair": "yes" },
"meta": {
"building": "yes",
"check_date:opening_hours": "2023-04-20",
"fee": "yes",
"tourism": "museum"
},
"score": 1,
"distance": 0,
"confidence": 1
}
],
"total": 12,
"executionTime": 38,
"metadata": { "shardsSearched": 1, "cacheHit": false },
"queryStats": { "pruningEfficiency": 0.92, "cellsSearched": 1 }
}
Response Fields
| Field | Type | Description |
|---|---|---|
results | array | Array of matching places |
results[].id | string | Place ID ({h3_r5}_{type}{osm_id}) |
results[].name | string | Place name |
results[].categories | array | Categories computed from primary OSM tag |
results[].location | object | Geographic coordinates (lat, lon) |
results[].address | object | Address fields: housenumber, street, city, state, postcode, country, unit |
results[].contact | object | Contact info: phone, website, email, fax, facebook, instagram, etc. |
results[].hours | string | Raw opening_hours value |
results[].cuisine | array | Cuisine types (from OSM cuisine tag) |
results[].payment | object | Payment methods: cash, credit_cards, contactless, etc. |
results[].access | object | Accessibility: wheelchair, wheelchairToilets |
results[].brand | object | Brand info: name, wikidata, wikipedia |
results[].operator | string | Operator name |
results[].diet | object | Dietary options: vegetarian, vegan, halal, kosher, etc. |
results[].amenities | object | Amenities: outdoor_seating, internet_access, smoking, etc. |
results[].meta | object | Additional OSM tags not in other groups |
results[].score | number | Combined relevance score (0–1) |
results[].distance | number | Distance from search center (meters) |
results[].confidence | number | Data confidence score (0–1) |
results[].scores | object | Individual scoring components |
results[].scores.text | number | Text relevance score |
results[].scores.geo | number | Geographic proximity score |
total | integer | Total matching results |
executionTime | number | Query execution time (milliseconds) |
metadata.shardsSearched | integer | Number of shards searched |
metadata.cacheHit | boolean | Whether results were served from cache |
queryStats.pruningEfficiency | number | Cell pruning efficiency ratio |
queryStats.cellsSearched | integer | Number of H3 cells searched |
2. Get Place by ID
Retrieve detailed information about a specific place.
/v1/places/{placeId}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
placeId | string | Yes | Place ID (format: {h3_r5}_{type}{osm_id}) |
Example Request
curl -H "Authorization: Bearer your-api-token" \
"https://api.geog.dev/v1/places/8527526bfffffff_w157035918"
Example Response
Returns a single PlaceResult object with all available fields (same schema as search results). Response is cached for 1 hour (Cache-Control: public, max-age=3600).
Error Handling
Error Response Format
{
"error": "error_type",
"message": "Human-readable description",
"code": "MACHINE_READABLE_CODE"
}
Error Codes
| Code | Status | Description |
|---|---|---|
GEO_CONSTRAINT_REQUIRED | 400 | Missing required location field |
INVALID_LOCATION | 400 | Invalid latitude or longitude values |
INVALID_RADIUS | 400 | Radius must be a positive number |
RADIUS_EXCEEDED | 400 | Radius exceeds 100,000m maximum |
INVALID_JSON | 400 | Request body is not valid JSON |
VALIDATION_ERROR | 400 | Request parameters failed validation |
INVALID_ID | 400 | Place ID format is invalid |
NOT_FOUND | 404 | Place not found |
SERVICE_UNAVAILABLE | 503 | Search service is not configured |
INDEX_NOT_AVAILABLE | 503 | Search index is not available |
Best Practices
Search Optimization
Always provide a location. The
locationfield is required for all searches. The Places API uses H3 cell partitioning for efficient spatial queries.Use appropriate radius. Smaller radii return faster results. Start with the default 5,000m and increase only if needed.
Combine text and category filters. Use
queryfor free-text search andcategoriesfor structured filtering together for best results.Paginate large result sets. Use
limitandoffsetfor pagination instead of requesting all results at once.
Scoring Weights
Adjust the weights parameter to tune result ranking:
{
"weights": { "text": 0.7, "geo": 0.3 }
}
- Higher
textweight prioritizes name matches - Higher
geoweight prioritizes proximity to the search center - When omitted, the API uses balanced default weights
Caching
- Place details (
GET /v1/places/{id}) are stable and suitable for client-side caching - Search results may change as the index updates; cache with shorter TTLs
See Also
- Authentication — Token types and scopes
- Rate Limiting — Rate limit details
- Errors — Error codes and handling