June 8, 2026 · 7 min read · Data

When "New York City" really means 87 neighborhoods.

A user types "best restaurants in NYC." Your raw geo store has Manhattan, Brooklyn, Queens, the Bronx, Staten Island, plus 80-something named neighborhoods inside them. If you naively run that query against a flat city table, you get either nothing or five disjoint slices. Megacity rollup is the unglamorous primitive that makes travel APIs actually usable.

Megacities aren't single rows

Open Street Map will happily tell you that Hell's Kitchen, Times Square, the Upper West Side, Williamsburg and Astoria are all valid place polygons. Foursquare has venues attributed to each. Wikidata has sub-articles. None of these are wrong — they're just at the wrong granularity for "give me NYC restaurants."

The problem is structurally identical across every megacity:

What the rollup looks like in the API

Each city in /v1/cities has two flags that do the work:

GET /v1/cities/nyc

{
  "city_id": "nyc",
  "name": "New York City",
  "iso_alpha2": "US",
  "aggregates_neighborhoods": true,
  "child_city_count": 87,
  "tier": 1,
  "context": { ... }
}

aggregates_neighborhoods: true tells the API to fan out: any sub-query (restaurants, hotels, attractions) traverses the children before ranking. child_city_count is metadata — it tells your client how big the underlying union is, which matters if you're showing a "based on N neighborhoods" disclosure or paginating.

The same shape works in London:

GET /v1/cities/london

{
  "city_id": "london",
  "name": "London",
  "iso_alpha2": "GB",
  "aggregates_neighborhoods": true,
  "child_city_count": 16,
  "tier": 1
}

16 is the major borough count we expose to clients. The actual union underneath is broader, but the customer-facing number is the one that maps to how Londoners and tourists describe the city.

Why this isn't a join you should write yourself

Every team that builds a travel product hits this and reaches for the same instinct: parent_id self-join, recursive CTE, done. It looks easy until you handle:

Each of these is an editorial call. We make the call once, you consume it as a flag.

Practical example: ranking restaurants in Tokyo

GET /v1/restaurants?city_id=tokyo&limit=8

# returns Michelin-starred venues across Shibuya, Minato,
# Chiyoda, Ginza, etc., ordered by tier, with each venue
# tagged to its actual ward — but presented as "Tokyo"
# results to the user.

Without the rollup, the same query at the ward level would miss Sukiyabashi Jiro (Chuo) entirely if your user typed "Tokyo" and your code passed it as a city literal. With the rollup it just works.

What this means for your product

Megacity rollup is one of those primitives that's invisible when it works and catastrophic when it doesn't. The bug manifests as "user typed Paris, our app showed 12 results from one arrondissement, competitor showed 47 from across the city." You don't lose users to that bug — they leave silently and never come back.

Use aggregates_neighborhoods as a feature flag in your UI: when it's true, you can confidently show "across N neighborhoods" framing. When it's false, the city is small enough that one query is the answer.

Sign up — free See India coverage →