Extracting Structured Fields From a Restaurant Reservation Confirmation
10 min read · updated August 11, 2026
A reservation email says “Saturday 7 November, 7:30 PM, table for 4”. There is no timezone in that sentence, and the single most damaging thing an extraction pipeline can do is supply one by normalising to UTC using whatever zone the server happens to run in.
The email states a wall time and nothing else
Confirmation emails are written for a human standing in front of the restaurant. They state a local wall time because that is the only representation that is useful to the reader: the time the clock on the wall will show when they are supposed to arrive. Almost none of them carry an explicit offset, and the ones that do are usually carrying the offset of the sending system rather than the venue.
A model asked for “the reservation datetime” will helpfully return an ISO 8601 string, and an ISO 8601 string with a Z on the end is a claim about an instant in absolute time. That claim was not in the document. It was manufactured, from an assumption, at extraction time, and it is unrecoverable afterwards — once 2026-11-07T19:30 has become 2026-11-07T19:30:00Z, you cannot tell whether the original was half past seven in London or half past seven in Chicago, and the difference is six hours.
The rule is therefore blunt: extract the wall time as written, as a value with no offset, and put the zone in a separate field derived by a separate step that can fail. A datetime you cannot resolve is a datetime with a null zone, which is honest. A datetime resolved with the wrong zone looks perfect and is six hours out.
bad "starts_at": "2026-11-07T19:30:00Z"
good "local_time": "2026-11-07T19:30",
"time_zone": "America/Chicago",
"zone_source": "geocoded_from_venue_address",
"instant_utc": "2026-11-08T01:30:00Z"Keep the instant if you need it for sorting, but keep it as something derived from two fields you can audit, not as the primary representation. If the zone later turns out to be wrong, the wall time is still right and the instant can be recomputed.
The zone comes from the venue, not the reader
There are four candidate zones floating around a reservation email and only one of them is correct. There is the zone of the server doing the extraction, which is irrelevant. There is the zone in the email’s own Date header, which belongs to the booking platform’s mail server and is frequently UTC. There is the recipient’s zone, which matters for reminders but not for the booking. And there is the zone of the restaurant, which is the answer.
Getting it means resolving the venue’s address to an IANA zone identifier. That is a geocoding problem, and the tempting shortcut — mapping a country or a state to a zone — is wrong often enough to matter. A US state is not a timezone: Florida, Tennessee, Kentucky, Indiana, Oregon and several others are split across two zones, and a restaurant in Pensacola is in a different zone from one in Miami. Arizona does not observe daylight saving time while the Navajo Nation within it does. Country-level mapping is worse still, since large countries span many zones.
Resolve to a coordinate and then to a zone polygon, and store the IANA identifier — America/Chicago, not CST and not UTC-6. Abbreviations are ambiguous across the world and offsets are only true for part of the year, which is the subject of the next section. Where the address is incomplete or the geocode is low-confidence, record the zone as unknown rather than guessing, in the manner missing required field handling describes; a reservation with a known wall time and an unknown zone is a usable record for a human and an honest one for a machine.
Two nights a year the arithmetic breaks
Once you have a wall time and a zone, converting to an instant is almost always unambiguous. Twice a year, in zones that observe daylight saving, it is not, and both cases produce silent errors in naive code.
- The night clocks go back, some wall times occur twice. In US zones under the current rule, daylight saving ends on the first Sunday in November, which in 2026 is 1 November. On that morning, 01:30 local happens once at UTC-4 and again an hour later at UTC-5. A 1:30am booking is genuinely ambiguous, and most date libraries resolve it to the first occurrence without telling you.
- The night clocks go forward, some wall times do not exist. Daylight saving begins on the second Sunday in March, 8 March in 2026. At 02:00 the clocks jump to 03:00, so 02:30 local never happens. Asked to convert it, libraries variously shift forward an hour, shift back an hour, or throw.
Restaurants do not usually book tables at half past one in the morning, so this is a small problem for reservations and a large one the moment the same pipeline handles hotel check-outs, flight departures, on-call rotas or anything else spanning the small hours. Handle it by asking your date library what it does at a gap and a fold and configuring it explicitly, rather than discovering the answer twice a year.
Party size is three fields
The other field on a reservation looks trivial and is not. Booking confirmations express the party as “Table for 4”, “4 guests”, “2 adults, 2 children”, “Party of four” or “4 (incl. 1 highchair)”. Those are not all the same statement.
A total count is what the restaurant seats. A composition — adults, children, infants — is what determines the table and sometimes the menu, and an infant in arms may or may not be included in the total depending on the platform. And accessibility or equipment notes, a highchair or a wheelchair space, are a third thing that occasionally gets folded into the number. Extract the total as an integer, the composition as an optional breakdown, and requirements as a separate list, then check that the breakdown sums to the total and flag it when it does not rather than trusting either.
Written-out numerals are worth a specific note because they are common in this document type and models handle them inconsistently in languages other than English. “Party of four” and “Tisch für vier Personen” both mean 4; asking for an integer and getting the string four back is a schema failure rather than a reading failure, and is fixed by constraining the field type at the API level rather than by pleading in the prompt. The general ground on structured output support covers how much of that constraint each provider will actually enforce.
What to store
A reservation record that survives contact with reality holds the document’s own words, the derived interpretation and the provenance of the derivation:
{
"venue": { "name": "Example Bistro",
"address": "1400 W Randolph St, Chicago, IL 60607" },
"local_time": "2026-11-07T19:30",
"time_zone": "America/Chicago",
"zone_source": "geocoded_from_venue_address",
"zone_confidence": "high",
"party": { "total": 4, "adults": 2, "children": 2 },
"requirements": ["1 highchair"],
"confirmation_code": "BX7Q2M",
"raw_time_text": "Saturday 7 November, 7:30 PM"
}Keeping raw_time_text costs nothing and is the field you will want the first time a customer says the booking was at a different time. Everything else on the record is an interpretation; that one is evidence. The same discipline applies with more force to a flight itinerary, where departure and arrival are stated in two different local zones on the same line.