01Overview
QPlace turns one ordinary sentence into a screen you can act on.
You say what you want, typed or out loud. QPlace reads what kind of answer the question calls for and builds the screen to match. There is nothing to choose first: no filter row, no date picker, no party-size stepper, no tabs. The city, the mood, the dates and who is coming are read out of the sentence itself.
Underneath sits a full intent-to-interface system: real venues from Google Places, live events from Ticketmaster, interpretation by Gemini, composition by a server-driven UI engine with a hard authority boundary. The model never invents a place, an address or an opening hour. That is an architectural constraint, and the rest of this page is how it is enforced.
Different question, different screen
21,393
lines of Swift
1,677
lines in the discovery edge function
16
widget templates
4
depth levels
454
contract tests
02
Zero cognitive load on the surface. The intelligence lives below the waterline.
Reached by asking, never by configuring.
02Design thesis
The Iceberg: one input above the waterline.
Most discovery apps make the user do the interface work: pick a category, set a date, adjust filters, then read a list that looks the same for everyone. QPlace inverts the contract. The surface holds exactly one input. Everything the product knows how to do lives below the waterline and is reached by asking, never by configuring.
The rule is falsifiable. Every proposed feature faces one question: does it force the user to fill in a form? If yes, it belongs to a different app. Even personalization obeys it: preferences accumulate from use, with the app asking once, in one line, whether it should remember something the user already said.
The response side chases a quality the project calls the monogram: every answer should read as freshly composed for this person, this moment. That feeling is engineered from three levers. Combinatorics: few templates, many orderings; a 16-entry catalog already yields hundreds of distinct screens. Query-specific framing: headers are written from the ask, never from a category label. Context awareness: a Friday-night ask and a Tuesday-noon ask should not compose the same screen.
03
A sentence goes in. A decision comes back.
Six intents, two lexical channels, zero milliseconds.
03Anatomy of a turn
One turn through the brain.
Every turn runs the same pipeline, serialized per session. A heuristic intent router classifies the sentence into one of six intents (discover, detail, modify, commit, plan, anomaly) deterministically, offline, in effectively zero milliseconds. Routing was deliberately not handed to a model; the measured reasons live in the Orchestration section below.
SessionViewModel.send
→ QPlaceBrain.respond serialized per session
→ IntentRouter discover · detail · modify · commit · plan · anomaly
→ dispatch across the depth ladder
→ Composer widgets + transitions + next moves
→ UIParser.encode → wire JSON → render
The router reads two lexical channels. English markers match as exact tokens or whole phrases; Turkish markers are stems matched as word prefixes over diacritic-folded text, because the language inflects at the end of the word: eczane, eczaneye, eczaneler, eczanesi are one word asked four ways. Two rules are test-enforced: no Turkish stem shorter than four letters, and no Turkish stem may ever fire on an English prompt.
Follow-ups are split three ways, and the distinction is what stops a session from resetting itself. A constraint narrows the running query: “somewhere cheaper” stacks on top. A subject replaces it: a pivot to a pharmacy re-classifies the whole ask. And steering never enters the query at all: “show me other options” excludes the place ids already on screen from the refetch, so other options really are other places.
04
The model selects. Deterministic code hydrates.
A fixed catalog, a validator, and a floor that cannot blank.
04Server-driven UI
The wire carries a decision, not a screen.
What comes back over the wire is not markup and not prose. It is a selection: which widgets out of a fixed 16-entry catalog, in what order, at what weight, plus the moves the user can take next. Composition becomes a per-question decision rather than a layout committed at build time, and the SwiftUI client is a thin renderer for whatever that decision produced. Every brain, the on-device one today and any future server orchestrator, answers through the same door with byte-identical encoding.
{
"session_id": "s-42",
"depth_level": 1,
"input_mode": "open", // open · focused · locked
"merge_strategy": "reset", // reset · append · replace_matching
"header": "Rooftops with a view in Kadıköy",
"widgets": [
{ "widget_type": "map", "payload": { "pins": [ … ] } },
{ "widget_type": "place_card", "payload": { "place_id": "…", "rating": 4.6, … } },
{ "widget_type": "carousel", "payload": { … } }
],
"next_moves": [
{ "label": "Take me there", "kind": "commit",
"fulfillment": { "action": "navigate", … } }
]
}The load-bearing law is select ≠ hydrate. A model writes queries and picks catalog ids; deterministic Swift hydrates every payload from fetched data. The model is structurally unable to emit a coordinate, a place id or a photo URL. Between selection and screen stands a validator that drops, in order: unknown ids, entries foreign to the current depth level, entries whose data requirement the turn cannot meet, duplicates, and the second member of an exclusive pair. If nothing survives, a known-good recipe per intent takes over. The model can pick a weak combination from a fixed menu, never a broken one.
Decoding is tolerant end to end: unknown widget types are dropped at the parser, a malformed entry drops alone without killing the turn, and there is deliberately no placeholder view for an unknown widget type, so a hallucinated widget cannot reach the screen even in principle. Each turn becomes a page on a stack, and the stacking decision (first, replace, push) is a pure function extracted after two bugs that reached built versions proved it did not belong inside a view model. Its signature rule: a page that found nothing never buries a page that found something.
The catalog
05
Four depths. One input. No tabs.
The session deepens; the input narrows.
05The depth ladder
Four levels, each narrowing what a turn may touch.
A session descends a four-level ladder, and each level changes both what the brain may touch and how much interface is offered back.
| Level | Turn | Input mode | Typical answer |
|---|---|---|---|
| L1 | discover | open | map, hero place, carousels per category |
| L2 | detail | open | the focused place enriched, often one line |
| L3 | modify | focused, moves preferred | a re-query that swaps cards in place |
| L4 | commit / plan | locked, commit actions only | the locked plan with times and directions |
Response weight follows the same discipline, calibrated from the ask rather than the level alone: a superlative or an errand noun reads as minimal, a mood reads as rich, everything else as standard. Depth decays it, and the third refinement steps it down again, because a user who is narrowing does not want the answer growing.
Boundary enforcement is the point. By the time a decision is close, the open text field gives way to on-screen moves, so a session converges instead of wandering into decision fatigue. At L4 the weight rule bottoms out by design: the response goes minimal because the plan is the response. Back pops the page stack; a fresh mission stacks over the trail rather than erasing it, so nothing the user found is ever lost to a new idea.
06
Model calls are placed, not sprinkled.
Heuristics on device, Flash on the fetch, a foundation model on the frame.
06Orchestration
Three brains, each kept where it wins.
The governing law: no network hop that is not already fetching data. Routing, gating and the composition recipes run on device, deterministic and free. The server model rides along the one fetch that was happening anyway: Gemini 2.5 Flash interprets the ask, writes the retrieval plan and proposes the composition inside the Places round trip; its corrective second look, described in the loop section below, is the only model call that sees the actual results. Its freedom is bounded by construction: structured output against a response schema, a closed widget and move vocabulary, thinking budget zero, 1,280 output tokens.
Apple’s on-device foundation model keeps exactly one job in release. When a plan locks, it writes the plan’s title and closing line from mood and shape only, never the places, so it cannot assert a wrong fact. The bigger role was measured and declined: on a real device the on-device model routed 10 of 10 fresh-surface cases correctly, then refused all 33 mid-session cases, because mid-session prompts carry the place names on screen and the guardrail rejects any prompt containing Turkish words. ASCII-folding does not help; the guard detects the language, not the diacritics. The heuristic router scores 81 percent at a cost of zero, so the verdict stands until the guard goes multilingual.
Data enters through narrow doors. Google Places runs under a hard locationRestriction rectangle with a haversine cut behind it, because a soft bias was measured returning confident matches from other countries. Ticketmaster events are refreshed per city on a schedule, since trending events are city-uniform and should never cost a personal request. Trip-scale asks hand off to a dedicated travel planner. And in front of everything sits a cost ladder: per-minute rate limits, a 30-minute response cache on a roughly 110-meter coordinate grid, per-device and global daily quotas, and only then a model call.
07
Showing less beats showing false.
The gate drops what the data cannot prove.
07The honesty gates
Two of the APIs never say no. The gates say it for them.
The two most expensive lessons in the project generalize to any grounded AI product. Apple’s geocoder is a search, not a lookup: “Cappadocia” once resolved to Via Cappadocia, a street in Rome, and because the anchor set the session origin, the next search genuinely ran in Rome. Google Places is a search, not a filter: asked for rooftop bars in a sparse district, it returns pubs, ranked confidently. Neither API ever returns an honest no-match. Both fixes are pure functions with their measured tables nailed down as test fixtures.
GeocodeMatch requires a candidate to actually name what was asked, front-anchored: “Provence” may extend to “Provence-Alpes-Côte d’Azur”, but “Via Cappadocia” may not claim “Cappadocia”, which occurs mid-string. A curated 27-pair Turkish and English exonym table widens which spellings may answer, never what an answer must look like. PlaceRelevance checks two claim classes in opposite directions: a cuisine drops only on a contradiction of Google’s own primary type, because a sushi place typed plain restaurant is still real, while a venue feature such as rooftop, garden or waterfront needs positive evidence in the venue’s name, word-prefix matched because “sky” occurs inside “whisky” and a whisky bar is not a rooftop.
The deliberate cost is stated rather than hidden: a real rooftop whose name does not say so gets dropped, because less beats false. And when the gate empties a page, the empty page ships as a real answer with a pivot rail, stacked on top so the last real results stay one tap back.
Beyoğlu, İstanbul
Measured“manzaralı bir çatı katı bar · a rooftop bar with a view” · 15 raw results, 7 survive the gate, every one a genuine rooftop
Ümraniye, same minute
Measured“the same sentence” · Google padded the categories with a döner shop, a hotel and a golf restaurant; the gate dropped all four results and the page shipped honestly empty
08
The brain judges what came back, then asks one better question.
One corrective hop, narrated as it happens.
08The agentic loop
Search, judge, ask again, then compose.
After the Places fan-out, the server judges every category with a TypeScript replay of the client’s relevance gate, generated from the same shared tables. A category the gate would leave with fewer than two places is thin, and thin categories earn exactly one corrective hop: a second, colder model call that sees the failed results themselves, the names, the types, the evidence the gate wants and the places that already survive, and writes one replacement query per thin category, or declines. The server never removes a place from the response. Authority stays with the client’s gate, so a parity bug can cost a wasted retry, never a wrong screen.
Measured on the district behind the canonical empty page: the loop lifted the answer from three verified rooftops to five, where the pre-loop deploy had measured zero. The dense control cost the judge microseconds and skipped the hop entirely. The decline path is real: asked for cheap sushi where there is none, the hop requeried nothing. An honest empty beats a second round of padding.
When real work starts, the response becomes a server-sent event stream, narrated from a closed four-phase vocabulary. Each phase is emitted at the moment the server enters that stage, never from a timer, and the final event carries byte-identical JSON to the non-streamed path, so the narration is honest by construction: it can inform, it cannot embellish. The client renders each line word by word, 90 milliseconds a word. A cache hit refuses to pretend and answers as plain JSON in under half a second.
One streamed turn, arrival times
MeasuredThe rail under an answer is a selection too. Three catalog moves exist: navigate, make it a plan, pivot. The model picks and orders them; the client hydrates every label, prompt and coordinate, validates the pick and falls back to a fixed rail when the pick fails. The measured shapes behave like product design rather than model whim: an errand’s rail says GO and nothing else, a mood browses first, a trip leads with the plan.
09
Coffee at 08:00 and coffee at 23:00 are not the same question.
One clock, a closed vocabulary, both ends of the wire.
09Time, place and memory
Context is computed once and spent everywhere.
One clock computes the hour, the weekday and the season in a single place, hemisphere-aware by latitude, so July reads as summer in İstanbul and winter in Buenos Aires. The context is spent on both ends of the wire. The prompt carries seven explicit time bands, weekend mornings mean brunch, and winter drops self-initiated rooftop and garden ideas; on the device, Friday from 17:00 counts as the weekend and winter backfills the idea rail from an indoor reserve. One guard outranks all of it: the user’s own words. A January ask for a terrace still searches for terraces.
Same breakfast sentence, same coordinates, a Sunday and a Tuesday: Sunday came back as brunch and named the day in its own subtitle; Tuesday came back quick and practical. The weekday is genuinely read, not decorated.
Session memory follows the same discipline. Durable facts accumulate from use into a context object with a closed preference vocabulary, enumerated in the published privacy policy: things like budget, dietary needs, occasion and party size. The allowlist is enforced on both ends of the wire, on the principle that an allowlist enforced at only one end of the wire is not an allowlist. And the boundary was proved rather than asserted: a request carrying two junk keys, one of them a full sentence, produced a byte-identical response to the no-extras case. Identical bytes mean an identical cache key, which means the junk never reached the prompt at all.
10
If it was not measured, it is not claimed.
454 tests run the shipped code itself.
10Verification culture
The contract is pinned by tests on both sides of the wire.
454 test cases across 31 files run the production sources themselves: 38 files are symlinked from the app into a pure SwiftPM package, so the logic under test is the logic that ships, byte for byte. The discipline exists because two bugs that reached built versions came from decisions buried inside a main-actor view model where tests could not reach. Today every load-bearing decision, page stacking, both honesty gates, refinement classification, the SSE parser, the clock, is a pure value-to-value function.
The app and the edge function agree in exactly one file: a shared contract JSON carrying the level-1 widget catalog, the moves, the streaming phase vocabulary, the context keys and both relevance tables. The server derives its prompt blocks from it at load; the app pins every table character for character in tests. A one-sided edit turns a test red.
The project also keeps a ledger of measured dead ends so they are never retried: biasing the geocoder toward the session region made it worse, measuring the geocoder on macOS proves nothing about iOS, and fixing relevance in the prompt alone cannot stop a search API from padding. Every bench is written down with its sample size, including when that sample size is one. The most valued defect class in the codebase is not a crash; it is a claim the code does not keep.
11
Your plans never leave your phone.
What leaves the phone fits in one paragraph.
11Privacy
Little enough to enumerate.
There is no third-party analytics, no advertising SDK, no cross-app tracking and no IDFA. QPlace never shows the tracking prompt because there is nothing to track. Sessions and saved plans are stored on the phone and are never uploaded, backed up or read. Plan titles are written by Apple’s on-device model, on the phone. The microphone is live only while the voice button is held, and only the resulting text ever leaves.
What a search sends is short enough to list in full: the sentence, approximate coordinates, a neighborhood name computed on device, the local hour, weekday and season, the search radius and language, the shape of the turn (whether it refines the last ask, plus any open-now or price filter), at most the remembered preference keys the policy enumerates, and a random install identifier that is not Apple’s advertising identifier and is regenerated on reinstall. Account data, an email address and a subscription status, lives in Frankfurt. The policy’s own line: “It is written to be read, not to be survived.”
12
Say the word.
On the App Store.
12Release
On the App Store.
QPlace is an iOS app, built end to end by one person at Reconchille Studios, and it is on the App Store. The first three asks are free. QPlace Pro continues from there as a weekly or yearly subscription, priced in the app, with a three-day trial on yearly. Places data by Google.