Skip to content

Build1 publisher3 min readPublished

A 200-result ceiling splits Dubai into 1,760 tile-and-category crawl jobs

A Dubai directory project measured how deep a single Google Maps query will paginate. The answer decided the tile grid, the terminating condition and the budget guards in the crawler built on top of it.

The Engineer · Build desk

Illustration accompanying A 200-result ceiling splits Dubai into 1,760 tile-and-category crawl jobs

What happened

  • A single Google Maps query on SearchApi's engine stops at roughly 200 results. No one query can enumerate the businesses in a city.
  • On page 11 the response arrives with the local_results key absent from the body entirely, so a crawler gets missing results instead of an empty array.
  • Covering Dubai therefore takes 44 geographic squares crossed with 40 categories, a grid of 1,760 tile-and-category pairs.
  • The free plan command prints 1,250 first-page jobs against a 3,170 worst case and a 2,000 default budget, warning that full depth overruns by 1,170.
  • The MIT-licensed toolkit's live Dubai deployment stands on 1,400 SearchApi requests, which returned 15,246 unique businesses, about 10.9 per request.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The terminating condition for a paginated crawl has to be an existence check written before the first run, because a length test on a key that is not in the response fails at exactly the page where the crawl should stop.
  • cost Full depth across the grid prices at 3,170 credits against a 2,000 default budget, so what an operator buys with each run is pagination depth, the only setting the write-up says moves the bill.
  • decision With half the results landing outside the tile that found them, the grid cannot be treated as a partition, and anyone reusing this design has to dedup on place_id and reassign by coordinates.
  • exposure Restricting the takedown suppression list to place_id means a removal request is enforced on a stable key that survives a business renaming itself.

A loop that reads `data.local_results.length` throws a TypeError on page 11 [2]. One that reads `data.local_results?.length ?? 0` gets a zero and exits cleanly, for the wrong reason. The toolkit ships that response as a committed test fixture, so the next person does not spend a credit rediscovering it [3]. `FULL_PAGE` in the crawl loop is 20 [13], so roughly 200 results is about ten pages, and page 11 is the first request that buys nothing [1].

The ceiling number itself is soft. The write-up gives "roughly 200" from its own crawling and does not cite a documented limit [1]. For contrast it names the Places API Text Search ceiling: 20 results across three pages, 60 in all [8]. The Maps engine goes about three times deeper before it stops [2].

SearchApi's fit for this job is its location parameter. It takes Google's own format straight through as `ll=@lat,lng,zoom`, so no geocoding service sits between the tile grid and the query [19]. One request costs one credit. The cost model is a 3x3 lookup table [20]. Roughly 99.8% of results come back with `country_code` and `city`. That pair drives the in-city filter without a geocoder [10]. The write-up is published as part of SearchApi's developer ambassador programme, which the author says backs developers who ship something real on the APIs and leaves the work in their name [18].

Tiles overlap in what they return. Google answers from a radius, and about half the businesses landed in a tile other than the one whose query found them, so the pipeline reassigns each business by its own `gps_coordinates` [9], which arrive on every result [11]. `place_id` arrives on 100% of results and stays stable, and it is the dedup key, the database partition key, and the only value the takedown suppression list may hold [12]. Duplicate hits across neighbouring tiles collapse on that key. Each one still cost a credit to fetch.

Four guards sit outside the crawl loop: a mandatory `--yes`, a free dry run, a hard budget stop, and the plan-time drops [14]. The fifth is inside it. `shouldFetchNextPage` buys another page only when the last one came back with a full 20 results and at least 30% of them were new [13]. The retry predicate is one expression, true for 429 and for anything 500 or above [15]. The comment above it says rate limits and server errors are worth retrying, while a 400 or a 401 will fail identically forever and retrying only burns time and credits [16].

The Dubai run suggests the new-results check rarely stopped a fetch. No page returns more than 20 results, so 1,400 requests could have produced at most 28,000; the crawl kept 15,246 unique businesses, which puts the corpus-wide new-unique rate at about 54% against a 0.3 floor [3]. For 10.9 businesses per credit [7] to hold in another city, the density inside a similar grid has to be comparable and most of the category list has to return results. The planner already threw out 510 of the 1,760 pairs before any credit was spent [5]. As the write-up puts it, "Crawling law firms in the desert spends money to find nothing." [17]

What to watch

  • A published depth limit from SearchApi or Google would replace the measured "roughly 200" and let crawlers size maxPages directly.
  • If page 11 ever starts returning an empty local_results array, the committed fixture fails and every absent-key probe downstream needs revisiting.
  • Whether the suppression list stays place_id-only once removal requests start arriving by business name instead of by key.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories