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.

keyless Overture-native token-budgeted no ETL MIT licensed

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.

4What's actually in it

5Tools today

ToolAnswers
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