Grounding an LLM in local business data

Grounding an LLM means fetching the fact at the moment of the question instead of trusting what the model remembers. For local businesses that is the difference between an answer and a guess: opening hours, phone numbers and whether a place still exists all change faster than any training run. Below: how grounding differs from retrieval, the endpoint that returns records with a citation each, and what it costs in tokens.

Why an LLM invents business hours and phone numbers

Opening hours are cheap to invent. They are short, they follow an obvious pattern, and almost every business in the training data has some. A model that has never seen a particular dental practice will still produce "Monday to Friday, 9:00 to 18:00" for it, fluently, because that is what dental practices look like in aggregate.

Phone numbers behave the same way. The country code is right, the digit count is right, the formatting matches the local convention, and the number belongs to nobody. Nothing in the output signals which parts were recalled and which were assembled.

Then there is decay. Training data is a snapshot with a date on it, and a business that moved, changed its hours or closed permanently after that date leaves no trace. The model is not lying about the past; it is answering a question about the present with information about the past.

RAG vs grounding: they solve different problems

Retrieval-augmented generation puts documents you already hold in front of the model: your handbook, your tickets, your product docs. It works when the answer exists in a corpus and the hard part is finding the right passage.

Grounding calls a system that owns the fact, at the moment the question is asked. Nothing is indexed in advance, so nothing goes stale between rebuilds. If you scrape a directory into a vector store on Monday, your answers are exactly as fresh as Monday, and a business that changed its hours on Tuesday will be wrong until you rebuild.

For local business facts there is no corpus worth indexing anyway. The universe of businesses in one city is large, most of it is irrelevant to any given question, and the fields you actually need — hours, phone, whether it is open now — are the ones with the shortest shelf life.

The two combine sensibly. Retrieve your own policies from your own store; ground the outside world with a call. What you should not do is embed a business directory and call the result grounded.

The fact you needWhere it should liveWhy
Your refund policy, your handbookA vector store you rebuild on changeYou own it, it changes when you change it, and nobody else can answer it
A business's name, address, coordinatesYour own table, refreshed occasionallyStable for months; the place_id keeps rows matched across refreshes
Opening hours, phone, whether it is still openA call at question timeAny cached copy is wrong on the day the business changes it, and you will not be told
Emails, WhatsApp, socials from the websiteEither; cached thirty days by defaultContact points move slowly, and crawled_at tells you how old yours is

The grounding API for an LLM: one GET, one text block

GET /v1/ground/local is the same underlying data as place search, shaped for a context window rather than a database. Send q and a location written as "City, Country", and get back records trimmed to the fields an answer needs.

curl "https://api.locomint.io/v1/ground/local" -G \
  --data-urlencode "q=dentist" --data-urlencode "location=Lisbon, Portugal" \
  --data-urlencode "limit=3" \
  -H "X-API-Key: $LOCOMINT_KEY"

The response is { "data": [...], "text", "query", "location", "meta" }. Each record in data carries name, up to two categories, address, city, country_code, phone, website, rating, review_count, status, open_now and hours_today — and a citation.

limit defaults to 5 and stops at 10. That ceiling is deliberate: the constraint on a grounded answer is not how many businesses exist but how many fit in the prompt while leaving room for the model to reason. If you need fifty rows, you want search, not grounding.

The same call in Python, with httpx, straight through to the prompt string:

import httpx

API = "https://api.locomint.io/v1"
headers = {"X-API-Key": "lm_free_your_key_here"}

with httpx.Client(base_url=API, headers=headers, timeout=60) as client:
    r = client.get("/ground/local", params={
        "q": "dentist",
        "location": "Lisbon, Portugal",
        "limit": 3,
    }).json()

prompt = (
    "Answer only from these facts. Cite the place_id for every business you name.\n\n"
    + r["text"]
    + "\n\nQuestion: which of these is open latest today?"
)
print(prompt)
for place in r["data"]:
    print(place["name"], place["citation"]["place_id"], place["citation"]["fetched_at"])

Why the text block costs fewer tokens than the JSON

Passing the JSON array straight into a prompt works, and it wastes tokens on structure the model does not need. Every record repeats every field name, plus quotes, colons, braces and commas around them. Multiply that by ten records and a meaningful share of the prompt is punctuation.

The text field is the same facts as one line per business, which is what most teams end up writing by hand after the first week. The header names the query, and each line is pipe-separated:

3 businesses matching 'dentist' in Lisbon, Portugal:
1. Clinica Exemplo | Dentist | 4.7/5 (212 reviews) | Rua Exemplo 12, Lisboa | +351210000000 | https://exemplo.example | today 09:00-19:00
2. ...

Count the characters in one JSON record against one of those lines for your own data — the ratio depends on how many fields are populated, and that is exactly why we do not quote a percentage here. What is reliably true is that the JSON carries the field names once per record and the text carries them zero times.

Keep the JSON anyway. Use text for the prompt and data for your own code: rendering a link, storing the place_id, deciding whether status is permanently_closed before you show the business at all.

Always send the country. A bare city name resolves against a default region, and it will not be yours. In our daily canary runs "Warsaw" came back as Warsaw, Indiana. Check meta.geocoded_location in the response the first time you use a new city, and put the rule in the system prompt if a model is composing the query.

When detail=true is worth the second credit

By default a grounded record has today's hours and no contact data beyond the listed phone and website. Pass detail=true and the API fetches the full record for each result and reads the business website: all seven days of hours, plus emails, whatsapp and contact_form_url. Two credits per business instead of one.

It is worth it when the question is about a day that is not today ("is anywhere open on Sunday"), or when the answer ends in an action — writing to the business, or handing the user a way to contact it. It is not worth it for a ranking question, where rating, review count and distance decide the answer and the contact fields are never used.

The cost of detail is also latency: each record means a website fetch, and the API caps that work rather than hanging. A site that does not answer leaves the summary in place rather than failing the whole call, so a partial answer still grounds the model. Websites that refuse automated visitors return a 403 and are left alone, not retried through another route.

Before either, GET /v1/places/count is free and tells you whether a query matches anything at all. An agent that counts first stops asking for ten records in a town that has two.

What hours_today does not tell you

Two fields carry the time-sensitive answer, and they are not the same thing. hours_today is the day's opening periods as the business lists them, formatted 09:00-19:00, with a second range when there is a lunch break. open_now is a boolean the source supplies, and it is absent for plenty of records.

The honest caveat: the weekday for hours_today is chosen by UTC date, not by the business's own timezone. For most of the day, in most of the world, those agree. Near midnight UTC they do not, and a business in Auckland or Los Angeles can be given the wrong day's hours. If your users are concentrated in one far-eastern or far-western timezone, take hours from the full record — all seven days, plus a timezone field — and pick the day yourself.

The times inside the field are the business's local clock, never converted. Do not let a model subtract an offset from them.

How to cite a business fact in a generated answer

Every record carries citation: { place_id, source, fetched_at }. The place_id is the stable identifier for the business, source names which data path answered, and fetched_at is when the record was read.

That last field is the one that matters in an interface. "Open until 19:00 today, as of 14:20 UTC" is an honest claim; "open until 19:00" is a promise you cannot keep, because the business could have changed the listing ten minutes ago. Showing the timestamp also turns a complaint into a bug report: if a user says the hours are wrong, you know which fetch produced them.

FieldUse it for
citation.place_idLinking the sentence to the record, and diffing the same business across runs
citation.fetched_atThe "as of" timestamp shown next to any time-sensitive fact
citation.sourceWhich data path answered, when you run more than one
status, open_nowSuppressing closed businesses before they reach the prompt

Instruct the model to answer only from the supplied block and to say when a fact is missing. A model given three records and asked about a fourth business will invent one unless the prompt tells it not to; that instruction is cheaper than any amount of post-processing.

Where to start this week

Take one question your product already answers badly — "what time does X close" is the usual candidate — and put the text block in front of the model for that question only. Compare the two answers against the business's own website. Fifty questions is about 250 credits, inside two free months or one Starter month.

If the model is choosing when to look things up rather than following your code path, connect the tools instead of the endpoint: the MCP server exposes the same grounding call as local_search, and the article on adding an MCP server for local business data covers the setup. Either way the LLM Grounding API page lists every field, and the status page shows the daily success rate for the source before you depend on it.

Frequently asked questions

What is the difference between grounding and RAG?

RAG retrieves passages from a corpus you already hold and hopes the answer is in there. Grounding fetches the fact at the moment of the question from a system that owns it, so freshness comes from the call rather than from how recently you rebuilt an index. For local business data, where hours and phone numbers change constantly, grounding is the only one of the two that can be right.

Why do LLMs hallucinate business hours and phone numbers?

Because opening hours and phone numbers are short, high-frequency patterns that a model can produce fluently without having ever seen the specific business. Training data is a snapshot months or years old, and a listing that has since changed leaves no trace in the weights. The model has no way to tell a remembered fact from a plausible one, so it returns both with the same confidence.

What does the grounding endpoint cost?

One credit per business returned, or two with detail=true. Credits are reserved for the limit you asked for and settled to the number that actually came back, so a thin result costs less. GET /v1/places/count and GET /v1/usage are free, and the Free plan includes 200 credits a month with no card.