You search "dentist in Pune" with the Google Places API, page through the results, and the list ends. Not because Pune ran out of dentists, but because the API did.

Google's docs say it plainly: "Text Search (New) returns a maximum of 60 results across all pages, although this limit is subject to change." Each page holds at most 20 (pageSize is "between 1 and 20"), so you get three pages and then nothing.

If you need every business of one type in a city, one query will never get you there. The workaround is geometry: ask the same question many times, each time for a smaller piece of the map, and merge the answers.

The idea

  1. Draw a rectangle around the city.
  2. Run the text query restricted to that rectangle.
  3. If the rectangle came back full (60 results), assume it was truncated: split it into four smaller rectangles and query each.
  4. Keep splitting until a tile returns fewer than 60, or the tile gets too small to matter.
  5. Deduplicate by place ID, because tiles touch at the edges.

The API supports this directly. locationRestriction takes a rectangle, "represented as two diagonally opposite low and high points", where low is the south-west corner and high is the north-east corner.

The request

Text Search (New) is a POST to https://places.googleapis.com/v1/places:searchText. You must send a field mask; "If you omit the field mask, the method returns an error.". Ask only for the fields you need, since fields map to SKUs (places.id is in the ID Only SKU, places.displayName in Pro).

import json, os, urllib.request URL = "https://places.googleapis.com/v1/places:searchText" KEY = os.environ["PLACES_API_KEY"] FIELDS = "places.id,places.displayName,places.formattedAddress,nextPageToken" def search_tile(query, low, high): """Return all places for one rectangle (max 60) and whether it looked full.""" body = { "textQuery": query, "pageSize": 20, "locationRestriction": {"rectangle": { "low": {"latitude": low[0], "longitude": low[1]}, "high": {"latitude": high[0], "longitude": high[1]}, }}, } found = [] while True: req = urllib.request.Request(URL, data=json.dumps(body).encode(), method="POST", headers={"Content-Type": "application/json", "X-Goog-Api-Key": KEY, "X-Goog-FieldMask": FIELDS}) with urllib.request.urlopen(req) as r: data = json.load(r) found += data.get("places", []) token = data.get("nextPageToken") if not token: break body["pageToken"] = token return found, len(found) >= 60 

pageToken is "the nextPageToken from the response body of the previous page", so the loop just feeds it back until it disappears.

Split full tiles

def split(low, high): mid = ((low[0] + high[0]) / 2, (low[1] + high[1]) / 2) return [ (low, mid), ((low[0], mid[1]), (mid[0], high[1])), ((mid[0], low[1]), (high[0], mid[1])), (mid, high), ] def crawl(query, low, high, min_deg=0.01): seen = {} stack = [(low, high)] while stack: lo, hi = stack.pop() places, full = search_tile(query, lo, hi) for p in places: seen[p["id"]] = p if full and (hi[0] - lo[0]) > min_deg: stack.extend(split(lo, hi)) return list(seen.values()) 

min_deg stops the recursion in a dense block where even a tiny tile stays full. If you hit it, you have found a spot worth a narrower query (a neighbourhood name or a sub-category), not a smaller tile.

Run it

# Rough bounding box: replace with your own city's south-west and north-east corners. low, high = (18.40, 73.70), (18.65, 74.00) rows = crawl("dentist", low, high) print(len(rows), "unique places") for p in rows[:5]: print(p["displayName"]["text"], "|", p.get("formattedAddress", "")) 

Why not just vary the query?

A common first fix is to run "dentist", "dental clinic", "orthodontist" and "dental hospital" and merge the results. That helps with recall for synonyms, but every one of those queries still hits the same per-query ceiling. In a large city, four capped queries give you four overlapping slices of the same busy centre, and the outskirts never show up at all.

Tiling attacks the right problem. The cap is per query, so the fix is more queries over less ground. You can still combine both: run the tile crawl once per synonym and deduplicate on id across all of them.

A useful habit is to log every tile that came back full, with its corners. That log tells you where the density is, and it is also the first thing to check when a client asks why one neighbourhood looks thin.

Things to watch

  • Cost scales with tiles. Every split is up to three more paid requests. Start with a box that hugs the city, not the whole district.
  • Edges overlap. Deduplicating on id is not optional.
  • The cap may change. Google says the 60 limit "is subject to change". Read the count instead of hard-coding three pages, as the loop above does.
  • A name and address is not a lead yet. You still need to check which places have a website, a phone number, or an email before outreach.

That last step is the part we automate at Mantis. If you would rather describe the list in plain words than run the crawler, the full comparison of five ways to build a city list, including this one, is in our guide.

I work on Mantis. This post was drafted with AI assistance and checked against Google's Text Search (New) documentation.