PlaceRoot
An MCP server that answers spatial questions from Overture Maps open data — queried live from the public GeoParquet release with DuckDB. No API key. No vendor platform. Every answer sized for an agent's context window, not a browser.
1Add it to your agent
Claude Desktop / Claude Code config — no signup, no API key to paste in.
{
"mcpServers": {
"placeroot": {
"command": "uvx",
"args": ["placeroot"]
}
}
}
Or run it directly: uv run placeroot starts the stdio MCP server.
2See the difference a token budget makes
One real query — coffee shops within 500m — run against
this repo's committed offline test fixture, two ways: what a raw
GeoJSON dump looks like, and what PlaceRoot's find_places
actually returns.
Raw GeoJSON ~45,000 tokens
typical raw GeoJSON for this query — full Overture Places schema, all categories within radius, no ranking or truncation
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "08f2a10793b0a2b1030037c9c1234567",
"geometry": { "type": "Point", "coordinates": [-73.899751, 40.699798] },
"bbox": [-73.899751, 40.699798, -73.899751, 40.699798],
"properties": {
"version": 3,
"sources": [
{ "property": "", "dataset": "meta",
"record_id": "1234567890123456789",
"update_time": "2024-11-15T00:00:00.000Z", "confidence": 0.98 },
{ "property": "names", "dataset": "Microsoft", "record_id": null,
"update_time": "2024-10-02T00:00:00.000Z", "confidence": 0.91 }
],
"names": { "primary": "Cluster Place 160", "common": null, "rules": null },
"categories": { "primary": "coffee_shop",
"alternate": ["cafe", "breakfast_brunch_restaurant"] },
"confidence": 0.67,
"websites": ["https://example-coffee-160.example.com"],
"socials": ["https://www.instagram.com/clusterplace160",
"https://www.facebook.com/clusterplace160"],
"emails": ["hello@example-coffee-160.example.com"],
"phones": ["+12125550160"],
"brand": { "names": { "primary": null, "common": null, "rules": null },
"wikidata": null },
"addresses": [{ "freeform": "160 Cluster Ave", "locality": "New York",
"postcode": "10003", "region": "NY", "country": "US" }],
"operating_status": "open"
}
},
{
"type": "Feature",
"id": "08f2a10793b0a2b1030037c9c7654321",
"geometry": { "type": "Point", "coordinates": [-73.900576, 40.699636] },
"bbox": [-73.900576, 40.699636, -73.900576, 40.699636],
"properties": {
"version": 2,
"sources": [
{ "property": "", "dataset": "meta",
"record_id": "9876543210987654321",
"update_time": "2024-08-03T00:00:00.000Z", "confidence": 0.94 }
],
"names": { "primary": "Cluster Place 000", "common": null, "rules": null },
"categories": { "primary": "coffee_shop", "alternate": ["cafe"] },
"confidence": 0.63,
"websites": [],
"socials": [],
"emails": [],
"phones": ["+12125550100"],
"brand": { "names": { "primary": null, "common": null, "rules": null },
"wikidata": null },
"addresses": [{ "freeform": "100 Cluster Ave", "locality": "New York",
"postcode": "10003", "region": "NY", "country": "US" }],
"operating_status": "closed_permanently"
}
}
/* … ~148 more features. Overture's row-group pruning gets you the
right tile fast; it does not shrink the payload once it lands.
Each feature above runs ~291 tokens (measured, chars/4) at this
schema depth — ~150 places is a normal count within 500m of a
dense downtown block across all categories, not just coffee. */
]
}
~45,000 = 291 tokens/feature (measured: len(json.dumps(feature))
// 4 against one representative Overture Places record, full native
schema) × ~150 features, the typical all-category place count within
500m of a dense downtown intersection. This repo's committed test
fixture only has 210 synthetic rows total, too small to reproduce
that count directly — the per-feature token cost above is real and
measured against this repo's actual code paths.
PlaceRoot 501 tokens
measured: find_places(lat, lon, 500, category="coffee_shop")
against the committed offline fixture
{
"results": [
{
"name": "Cluster Place 160",
"category": "coffee_shop",
"basic_category": "coffee_shop",
"operating_status": "open",
"confidence": 0.67,
"lat": 40.699798,
"lon": -73.899751,
"distance_m": 31.0
},
{
"name": "Cluster Place 000",
"category": "coffee_shop",
"basic_category": "coffee_shop",
"operating_status": "closed_permanently",
"confidence": 0.63,
"lat": 40.699636,
"lon": -73.900576,
"distance_m": 63.0
},
{
"name": "Cluster Place 104",
"category": "coffee_shop",
"basic_category": "coffee_shop",
"operating_status": "open",
"confidence": 0.88,
"lat": 40.700003,
"lon": -73.898479,
"distance_m": 128.0
}
/* … 7 more, nearest-first, same shape. All 10 results together:
2005 JSON characters, 501 estimated tokens (chars/4 heuristic,
same one placeroot.budget uses to enforce its own 2000-token
default budget). */
]
}
Reproduce it: uv run python -c "from placeroot import overture,
budget; overture.set_data_path('tests/fixtures/places.parquet');
r=overture.find_places(40.7,-73.9,500,category='coffee_shop',limit=10);
print(budget.estimate_tokens({'results': r}))" — real output
against the fixture committed in this repo, not a rounded guess.
3See it on a map — no key, no CDN
The same query, rendered by render_map /
placeroot.mapview into one self-contained HTML file:
inline CSS and JS, vector markers, click-to-open popups, a scale bar,
zero network requests when opened. This is a real generated artifact,
not a screenshot.
Open the map artifact directly — same file, full window.
4What's actually in it
- Keyless. Queries run directly against Overture's public S3 GeoParquet release with DuckDB — no signup, no API key, no vendor platform on the critical path.
- Fresh, not a stale snapshot. Every place carries
Overture's
operating_statusand aconfidencescore, contributed by Meta, Uber, TomTom, and others — an agent can reason about whether a place is actually still open. - Token-budgeted by design. Every tool response is
built to fit roughly 2K tokens; when a result would blow past that,
the response is truncated (rows dropped nearest-first-preserved,
then optional fields stripped) and says so with
truncated: trueandomitted_count, rather than silently dumping more than an agent can use. - A local tile cache. The first query touching a 1°×1° tile materializes it to disk; every later query touching that tile reads local parquet instead of rescanning S3 — the design target is warm queries under 500ms, and it keeps working if the network doesn't.
- GERS ids in every response. Each place carries Overture's stable GERS identifier — a cross-session, cross-tool reference an agent can hold onto and resolve later, instead of re-searching by name and hoping for the same match.
- No ETL. There's no intermediate database or import job — every query reads Overture's GeoParquet directly, with bbox column pushdown to prune row groups.
- MIT licensed. No usage tier, no metered key.
5Tools today
| Tool | Answers |
|---|---|
find_places |
Named places near a point, nearest first, with category, confidence, and operating status |
summarize_area |
What's in an area: total places and top categories |
render_map |
Any find_places/summarize_area result
as a self-contained interactive HTML map |