I’m building an app that predicts wait times for lines at restaurants. It uses Google review counts to pick out candidate places that tend to have lines, then color-codes them on a map.

The design was simple: crawl once a week and store the place data in Firestore. Rating, review count, name, location. After that, the app just reads it. It’s fast, and it keeps API calls down.

That design ran head-first into the Places API terms.

What follows is a record of the calls we made after reading the terms ourselves. It is not legal advice. If you’re actually building something, read the current terms yourself.

Almost nothing is allowed to be stored

After reading through the terms, here’s how caching breaks down.

DataCan it be stored?
Place IDYes, indefinitely
Latitude/longitudeUp to 30 days
Rating (rating)Generally prohibited
Review count (userRatingCount)Generally prohibited
Name (displayName)Generally prohibited
Opening hours (regularOpeningHours)Generally prohibited

The only explicit exceptions are place IDs (indefinitely) and latitude/longitude (30 days).

In other words, what I was about to do, “store ratings and review counts in Firestore and use them to pick candidates,” sits outside the exceptions the terms allow.

The fix: store derived values, never raw values

Here’s the rebuilt structure.

At crawl time:
  Fetch rating / userRatingCount from the Places API
    ↓
  Decide the "candidate rank" on the spot (in memory, then thrown away)
    ↓
  Store only the result in Firestore
    ↓
  Do NOT store the raw rating / userRatingCount; discard them

All we store are derived values we compute ourselves: candidate rank, map pin color, and predictions.

On the UI side, too, we never show raw ratings. Instead of “Reviews 4.3,” it says “Tends to have a line.” What the app sells was never review numbers in the first place. It sells wait-time predictions, so this didn’t cost us a feature.

Place IDs can be stored indefinitely, so I made the place ID the document ID. That’s compliant, and it came with a big side benefit: the same place can no longer be registered twice, structurally. Places submitted by users are also picked from Google search results and registered by place ID, so the deduplication logic itself became unnecessary.

Sometimes a design shaped around constraints ends up cleaner than the naive one.

Place names: I accept the risk

I’ll be honest. We do store place names (displayName).

Under the terms, that falls on the “general content = generally prohibited” side. But this app doesn’t work without place names. A map that just says “it’s crowded here” is useless.

So for names, I made the call to accept the residual risk as “essential to the feature.” On top of that, we:

  • Overwrite them on the weekly crawl (no holding on to stale data)
  • Show Google’s attribution

The framing is: we’re not compliant here; we operate knowing that we’ve accepted the risk. Not waving this through as “probably fine” made it much easier later when revisiting the decision.

Opening hours: there was an alternative, so we stopped storing them

Partway through, someone proposed: “We can get opening hours too. It costs nothing extra, so let’s store them.” It would improve prediction accuracy, so it was a tempting idea.

I rejected it. The deciding question was exactly the same one as with names.

“Does the app still work without it?”

For opening hours, there was a zero-risk alternative: keep default hours per category in a config file. Even rough values, like ramen shops 11:00 to 23:00 and cafés 8:00 to 20:00, were good enough for wait-time predictions.

Once an alternative exists, the “essential to the feature” argument doesn’t hold. The justification for adding one more gray-area stored field is clearly weaker than it was for names.

If I ever really want more accuracy, there’s an option: instead of storing the raw opening-hours structure, compute and store only a boolean for “open or not” for each 30-minute slot. It’s a derived value, so the exposure is smaller than storing the raw value. It isn’t fully compliant either, but I’ve noted it down as an option.

Attribution depends on whether there’s a map

This one affected the implementation, so I’m writing it down.

  • When shown on a map: the map itself displays the Google logo, so no extra attribution is needed
  • When shown on a screen without a map: the Google logo is required

In my app, the list screen that compares multiple places and the screen where you search for and pick a place fall into the second group. Every screen that shows Places content (like names) without a map does.

If you write the same implementation over and over, you’ll miss one somewhere, so I made it a shared component, and every screen that shows Places data uses it.

Temporary storage

There’s a feature where users suggest “please add this place.” Suggestions go through an approve/reject flow, so they need to be stored temporarily until a decision is made.

We allowed this, with conditions.

  • Latitude/longitude: going from suggestion to approval or rejection takes days to weeks, so it fits within the 30-day exception
  • Name: same risk as the permanent place master, but it’s held only temporarily (it moves over if approved and is deleted if rejected), so the exposure is actually smaller
  • Rating and review count: not stored, not even temporarily. Returning them to the client temporarily to show search results is fine, but persisting them to Firestore is prohibited

We also made it a rule to promptly delete rejected suggestions. Leaving them around only extends the exposure period.

The questions I used while designing

Looking up the actual clauses every time isn’t realistic, so I boiled the decisions down to 3 questions.

  1. Is it a place ID or latitude/longitude? If so, it can be stored
  2. Does the feature break if we don’t store it? If it still works, don’t store it
  3. Can a derived value I computed stand in for the raw value? If so, use that

The third one is the most useful. Not “store the rating,” but “store the rank I assigned after looking at the rating.” The idea of converting external data into your own data before storing it handled almost every case.

Summary

  • With the Places API, only place IDs can be stored indefinitely. Latitude/longitude: 30 days
  • Ratings, review counts, names and opening hours are generally not cacheable
  • A design that stores only derived values, not raw values avoids most of the problem
  • A field that has an alternative can’t justify being stored (opening hours was one)
  • If you show Places content on a screen without a map, Google attribution is required
  • Using the place ID as the document ID has a side benefit: duplicates structurally can’t happen

I redesigned around the terms after I already had something working. If I’d read the terms first, the rewrite wouldn’t have been needed. My rule now: when using an external API, read the caching section of the terms before the pricing page.


The cost design for the same app is in 3 apps, a 22 yen bill, and every place I tripped up while publishing to the store is collected here.