Knowhere

Changelog

Newest first. Dates are when the change reached the serving database — not when the code landed. Anything built but not yet serving belongs under an ## Unreleased heading, and takes a date only once it is deployed.

That distinction is not pedantry. Two entries below were once dated for a day they had not reached: the artifact serving on that date carried neither, and a query for the tags they promised answered zero rows rather than an error. A date here is a claim about the database a caller is talking to.

Unreleased

  • Resolving a place name in list_areas is no longer slow. Asking list_areas for "seattle" — the first call the golden path tells you to make — took 2.7 seconds on the serving database, because a name that is not an area was looked up by searching all sixty-five areas in turn, one text query each. It now reads the cross-area place index the build already writes, which is one query for all of them. The per-area search stays behind it, for a database built before that index existed and for an area the index does not cover, so nothing that resolved before stops resolving.

  • A city resolves to its centre, not to the middle of its bounding box. A place is often two entries in OpenStreetMap: a point where a mapper marked the centre, and an outline of its administrative boundary. We stored one coordinate for each, and for the outline that coordinate was the centre of its bounding box — which for a city shaped like Aurora, reaching east to the airport, is 13 km from anywhere anyone lives. The marked point now wins. Affects 237 places in colorado alone, which sat a median of 1.7 km from their own boundary's box centre.

2026-08-27

  • A bare postal code now geocodes. geocode with nothing but 80202 used to fail — every lane read the digits as a house number with no street, or as a name — and now resolves to the centre of the addresses carrying that code. Results come back with kind: "postcode". That centre covers a whole region, so the brief and check pipelines refuse it for the same reason they refuse a town centre: any distance measured from it would be wrong.

  • Accented street names are findable by their unaccented spelling. Searching "Canon City" used to return a point 50 km from Cañon City, confidently. Street names are now stored with accents folded, matching the search index, which had been folding them all along.

  • A named feature no longer loses to the signage named after it. "Denver International Airport" returned a tourism=information welcome sign 7.4 km from the aerodrome, because both matched the name exactly and nothing else separated them. Features OpenStreetMap marks as canonical now win that tie. 901 colorado names are decided by it.

  • A geocode result for a place carries osm_id, osm_type and a bounding box. It can be handed straight to search or near instead of being looked up again by hand. Absent fields mean the lane that answered cannot say.

  • Postal-code centres are more accurate. They were the mean of every address carrying the code, which a handful of mistagged rows dragged a long way — 91 of colorado's 417 well-populated codes moved by more than 1 km when this was corrected, and the worst by 14.5 km.

  • New Mexico's addresses are New Mexico's. An OpenAddresses file published as Sandoval County, New Mexico actually contained San Diego County, California — 1.1 million addresses, five degrees west of the state. They had been loaded as New Mexico addresses in every build, which is 44% of that area's addresses; searching or measuring to one gave a confident wrong answer. Addresses that fall outside the area they are being loaded into are now refused, and the build says how many and from which source.

  • A broad tag with a tight radius is no longer slow. A query like "every amenity within 30 m of this point" had to read every amenity in the state and check each one — 597 ms on colorado through the serving path, and worse on larger areas. It now reads only the features near the point: 2.3 ms for the same query. Wide-radius searches are unchanged. No answers change.

  • Tag searches naming a value are faster. A filter like [shop=yes] used to fetch about 43x more rows than it needed, because yes is a value on millions of features. Measured on the serving path, [shop=yes] on colorado went from 205 ms to 21 ms. No answers change, including for the few hundred tag values that contain an = of their own.

  • near reports an over-large side as TOO_MANY_RESULTS, not PAYLOAD_TOO_LARGE. The cap (100,000 features per side) is unchanged in value but is now applied inside the query rather than counted after both sides were already in memory, and the message names which of find or near overran.

  • walk_reach and walk_loop inherit the 50,000-row cap on the query that selects their candidate features. A find matching more than that is now an error rather than an unbounded read.

  • Results from query.execute and query.union come back in a different order. Neither ever promised one, and none of the tools built on them read it — but a script that happened to rely on the old order will see a different arrangement of the same rows. Sort explicitly if the order matters. (The old order was produced by a sort over two constant columns, which cost roughly half the query's time to compute nothing.)

  • A script can be stopped when the server is nearly out of memory.

    The row cap bounds one query. It cannot see what a script builds from several capped queries — an index keyed by grid cell, a list accumulated across a loop — and that is what actually exhausted the server: three live structures at once, only two of which were query results.

    A runtime script that drives the Go heap past its ceiling (512 MB by default) is now interrupted with "server memory is nearly exhausted" instead of the process being killed. Hold less at once: query a smaller area, or aggregate as you go rather than collecting every row and filtering afterwards.

    A script blocked inside a query is not interruptible this way — that half is what the row cap is for.

  • A query that matches too much is refused rather than truncated.

    query.execute and query.union in the runtime tool had no row limit at all. A statewide union of building=warehouse, building=industrial and building=factory matched enough rows to exhaust the server's memory: the process was killed and the site was unreachable for twenty minutes.

    Both now stop at 50,000 rows and return TOO_MANY_RESULTS instead of an answer. That is deliberately an error and not a quiet truncation — a script asking which industrial buildings have rail going in and none coming out computes a wrong answer from a truncated set, not a partial one, and nothing in the result would have told it which it got.

    If you hit it: narrow the area, add tag filters, or split the work by bounding box. If you only need how many there are, query.count() never materialises a row and has no cap.

  • One index for every area, instead of sixty-five. (needs a rebuild — the code is in, the speed arrives with the next database build.)

    Every autocomplete keystroke and every geocode without a state asked a question no single area can answer: which of the sixty-five has a street starting like this? The only way to ask was to ask each area in turn, and the server is one shared core reading a compressed 37 GB file, so the answer was routinely "9 of 65 areas, nothing found".

    The database now carries one street index and one place index covering every area, beside the per-area ones. Measured on a 65-area database through the same read path, on one core:

    what a keystroke does before after
    blake st 203 ms, 6-7 of 65 areas 40 ms, all 65
    3101 blake st 212-220 ms, 3-4 of 65 areas 138-149 ms, all 65
    east colfax ave 159-162 ms, 5-7 of 65 areas 57-71 ms, all 65
    washington sto 296-337 ms 41-65 ms
    3101 blake st denver 41 ms 41-50 ms, unchanged

    The important column is not the milliseconds, it is "of 65". A search that read a dozen areas and stopped had to say so — that is the "searched N of 65 areas" note under the dropdown — and a street in one of the other fifty simply did not appear. Cross-area searches are now complete rather than partial, so the note goes away.

    Geocoding a city without its state now works. geocode used to refuse "3101 Blake St, Denver" and ask for a state, because resolving "Denver" meant probing all sixty-five areas on the critical path of every lookup. With one place index it is a single query, so the address resolves as written.

    Two things deliberately kept: a lookup that already knows its area still reads that area's own tables, which are a smaller haystack (3 ms against 11); and a database built before this index still works, falling back to the old fan-out, so the code and the data can ship on their own schedules.

    The new index is about 1% of the artifact.

  • Autocomplete now narrows before it searches, which is why it answers at all. Typing 3101 blake st denver into the address field returned nothing, with a note saying 9 of 65 areas had been searched — on an address the database holds. Everything about that was a cost problem rather than a parsing one.

    What it costs to answer a keystroke, measured on a 65-area database through the same compressed read path the server uses, on one core — which is what the server has:

    what the search does cost
    read every area's street index 190 ms
    read one area's street index 3 ms
    read every area's place index (which city is this?) 22 ms

    The search used to read every street index first and only work out the city if that came back empty. On this hardware that spends the entire keystroke establishing nothing, and leaves the reading that would have answered with no time to run. It now asks the cheap question first: which areas have a Denver? Then it searches those. Same work, opposite order.

    What changed for a person typing:

    • 3101 blake st denver — was an empty list, now the address, complete rather than partial.
    • 3101 blake st denver co — a state costs no lookup at all, because the list of states is already in memory. About 15 ms end to end.
    • 3101 blake st — with nothing typed yet to narrow on, this is unchanged: the search reads what it can inside its budget and says how far it got.
    • The budget itself went from 150 ms to 400 ms, with the search-everything pass still capped at the old 150. An answer at 400 ms beats an empty list at 150, and the fragments that can be narrowed now finish well inside it.

    What this does not fix: a fragment with no city in it still cannot read 65 street indexes inside any sensible budget. The structural answer is one index covering every area instead of 65 — measured at 10 ms against 190 — and it is the entry above this one, arriving with the next database build.

  • Address autocomplete now finishes the address, however it is written. Autocomplete is the dropdown under the address field on /briefs: you type part of an address and it offers the real ones it could become, each carrying the coordinate it resolved to. It reads what you type with its own small parser — the full-address parser the geocoder uses refuses anything it cannot read whole, which is every keystroke before the last one — and that parser only understood a few of the ways people write an address.

    Measured against a real Colorado database: 120 addresses that are in it, each typed thirteen ways. Five of those ways used to fail, two of them completely — 0 of 120 — and every one of them is an ordinary thing to type. All thirteen now return the address.

    What was broken, and what it does now:

    • A postal code at the end emptied the dropdown. 150 s havana st aurora co 80012 — a complete, correct address — returned nothing, because the code and the state were searched for as part of the street name. The state and the code are now recognised and taken off first, and the city is worked out from what is left. 0 of 120 before, all 120 now.
    • Cities of three words were unreachable without a comma. Commerce City North, Old Colorado City and Piney Creek Ranches are real places; the field guessed at most two trailing words as a city name. Recognising the state and postal code separately freed the guess to run to three words.
    • A unit written before the address hid it. Apt 4B, 1500 Illinois St, Denver is how a delivery form and a mail merge write it, and the field read Apt 4B as the whole address. The unit is now lifted off the front.
    • Abbreviations that are not the start of the word they stand for. 2550 h rd could not reach 2550 H Road, because a half-typed word is matched as a prefix and rd is not the start of road. The same applies to ln/lane, ct/court, hwy/highway. Both spellings are now searched, so a person typing either finds the street.
    • A post office box offered nonsense. PO Box 417, Aurora, CO returned a list of unrelated streets, because the box number was read as a house number and the search shortened po box 417 until something matched. A box is a mail destination, not a place the database can locate, so the dropdown is now quiet.

    Two more shapes that failed, both found by reviewing the fix rather than by typing addresses:

    • Connecticut and Nebraska were invisible. Before the state is looked up, a street fragment is folded onto the spelling the database stores — which rewrites "ct" to "court" and "ne" to "northeast". Those are the only two US state codes that are also street words, so 100 asylum st hartford ct 06103 and 1234 farnam st omaha ne 68102 failed in exactly the way the headline fix was supposed to repair. The state is now read off the words as typed.
    • A state with no city after it narrowed nothing. washington st co resolved the "co" and then threw it away, because a bare two-letter locality is deliberately refused — "de" is Delaware and also the second keystroke of Denver. The state now comes back under its full name, which that check accepts.

    Two things it can do that it could not before:

    • A typed postal code decides between identical addresses. Where the same house number sits on the same street name in two places, the code sorts the matching one to the top — across the whole answer, not just within one state's share of it. It sorts rather than filters: the database's postal codes come from whichever source published each address, so requiring one would hide addresses that simply have none. Both sides are compared with their case and spacing removed, so a Canadian code typed h3b4w8 matches the stored H3B 4W8.
    • Intersections complete. washington st & wino now offers Washington Street & Winona Court, which is the form the geocoder resolves to the point where the two roads meet. The first street is named as the database spells it, not as it was typed — colfax & wino reads back as East Colfax Avenue & …. Only roads that pass through the same ~20 km grid cell are paired, which rules out no crossing that exists and rules out every Colfax-in-Denver-with-a-Broadway-in-Cortez. Where the crossing cannot be found at all, the street that is offered instead is marked as not precise, so a brief re-checks the address rather than measuring from a point on one of the two roads.

    One thing outside the dropdown changed with it: the geocoder read PO Box 417, Denver, CO — a box with no postal code after it — as house number 417 on a street called "PO Box", and looked that street up. It now reads the box and answers with the place, which is the most a box can resolve to. The second half of that was found by geocoding a box against the running server rather than by reading the code: recognising the box was not enough, because the reading that produced it had also lost the state, and the lookup then refused for want of an area on an address that names one.

    What it costs. Every re-reading of the input runs only after the plain reading has already come back with nothing, inside the same time budget. Measured over the same 120 addresses typed one character at a time, arms alternated three times: the middle keystroke went from 1.25 ms to 1.35 ms and the 95th from 3.69 ms to 3.79 ms, while the share of keystrokes answering with an empty list fell from 36.2% to 30.7%. About a tenth of a millisecond, for one keystroke in eighteen that used to show nothing.

    Both measurements are in the repository — TestSuggestWrittenForms and BenchmarkSuggestKeystrokes, run against a built database — rather than taken once and written down here.

  • Addresses outside the US now parse. "Parsing" here means splitting a typed address into its parts — house number, street, city, postcode — which is what the geocoder needs before it can look anything up. That splitting is done by a set of hand-written patterns, one per address shape, and it only ever knew the shapes somebody had written down: mostly American ones. Measured against a 12,832-address reference set, 66.98% of addresses parsed exactly right; it is now 76.15% — about 1,180 more addresses.

    Where the gains are. The United Kingdom went from 12% to 56%, France 14% to 92%, Australia 15% to 80%, Hungary 2% to 82%, New Zealand 4% to 59%, Russia 49% to 75%, Luxembourg and Taiwan from nothing at all to 88% and 70%. Taiwan is the clearest case: 177 addresses, none of them written in Latin script, and every existing pattern needed a comma, a US state or a numeric postcode to find its footing.

    Nothing was traded away to get it. The countries the patterns already read well are unchanged or better — Denmark 99.7%, Norway 100%, Lithuania 97%, the United States 93%.

    Four things did it. A postcode shape for the UK, which is the only anchor a British address reliably offers and which nothing understood. Address orders that were simply missing — the house number written before the street with the postcode after it (France), the city written first (Hungary), the postcode written last (Russia). Punctuation in town names: "lauf a.d.pegnitz", "zell (mosel)" and "sulzbach/saar" are ordinary German municipalities, and the city field accepted only letters and hyphens, so those addresses failed entirely and fell through to a pattern that read the trailing "de" as Delaware. And separating a suburb from its city, which is worth doing because the city is the field the search area is inferred from: "50181 bedburg kaster de" is Bedburg, not "Bedburg Kaster".

    Some fields still cannot be recovered without a place-name database, which is the honest limit of a pattern-based approach. Germany sits at 80% rather than higher for exactly that reason.

  • Address autocomplete no longer needs a comma. Typing 3101 blake st denver used to return nothing at all. The field read the whole string as a street name — it looked for a street literally called "Blake Street Denver", which does not exist — so an address that is in the database came back with an empty dropdown. It now reads the input a second way whenever the first way finds nothing, taking the last word or two as a place rather than as part of the street: denver, denver co and denver colorado all work, with or without the comma, and the search narrows to the areas that carry that city exactly as the comma'd form already did. Two safeguards. The second reading only runs when the plain one found nothing, so a half-typed street is never mistaken for a city — washington sto still completes to Washington Stone Lane instead of jumping to Stockton, California. And when the trailing word names a place the database has never heard of (a typo, or a town too small to carry a record), the field drops it and shows the street anyway, which is a near miss rather than an empty list. Both readings share one time budget, so this does not make a keystroke slower to answer.

  • Address autocomplete answers streets and addresses only. The dropdown used to offer settlements and postal codes as well. Both resolve to a centroid — the middle of a town, or the average of every address sharing a code — and a brief measured from one is wrong in every number, so the form refused them on submit. They were rows that could not be acted on. What the places index is genuinely good for on this surface is narrowing: name a city after the street and the search covers the areas that carry that name instead of all 65, which is unchanged.

2026-08-22

  • walk_loop: walking loops as a first-class tool. "I want to walk a mile in a loop" was a documented runtime recipe (#62); it is now one call. walk_loop spreads turnaround points around the origin and rejects any circuit that retraces more than 25% of itself — the tuned thresholds from the recipe, unchanged. Two things the recipe never had: a time target (target_minutes=60 is "walk for an hour", at 5 km/h), and themed loops via passing — a Knowhere query the circuit routes through, so "a mile loop that sees all the parks" is passing="nw[leisure=park](area=...)". Passing matches are snapped to the path network (a park with no path within 150 m is not somewhere a walk can pass) and visited in a bearing sweep, all of them when the full circuit holds up, pairs of them otherwise. Each loop reports the length it actually came out at, and none with a reason is a real answer — a cul-de-sac subdivision genuinely has no mile loop. Foot-only, within one area, target capped at 10 km. Works against the artifact already serving; no rebuild involved.

  • Address autocomplete on the brief form. The address field on /briefs now completes as you type — settlements, postal codes, streets and house numbers, across every area in the artifact rather than one you have to name first. Picking a suggestion carries the coordinate it resolved to into the brief, so the brief measures from what you chose instead of geocoding the text again and possibly landing on a different street of the same name. This is a web-form feature: no MCP tool changed, and geocode answers exactly what it answered before.

    Two columns were added to the artifact to support it — {area}_street_names.rank (how many addresses and road segments a name carries, which is what orders the suggestions) and a second alias column on {area}_street_search holding the name without a leading directional or French type word, so "Colfax" finds "East Colfax Avenue" and "Sainte-Catherine" finds "Rue Sainte-Catherine Ouest". Canadian postal codes complete too, with or without the space they are written with.

    A typed unit does not derail it: "551 Castle Dr Apt 6" falls back to the building rather than matching nothing, and a building says how many units its source listed. Which unit is not offered — nothing downstream measures per-unit, so a drive time from apartment 4B is a drive time from the building.

    A comma narrows the search. "Main St, Golden" or "Main St, CO" searches only the areas that carry that name — at most three — instead of all 64. That is both faster per keystroke and the difference between one Main Street and every Main Street on the continent. A city and a state written together ("Main St, Golden, CO") works too. A locality nothing answers to costs nothing: the search falls back to every area. Note that areas_total in the reply is how many areas the query was meant to cover, so a narrowed search that finished reports complete rather than partial.

    Unit counts changed with this build. A building's units are stored as bare identifiers now — "6", not "Unit 6". Sources spell one door several ways in the same file, and the step that counts a building's doors is a SQL DISTINCT, which read "Unit 6" and "6" as two. Measured on colorado, 3,046 addresses carried both spellings, so their counts were too high and their unit lists held the same door twice. Both are right now. The cost is that the designator words ("Apt", "Ste", "#") are no longer kept, so the artifact can say a building has six doors but not that door six is a suite rather than an apartment.

    Nothing has to be in the list for the form to work. A "Use what you typed" row is always the last option, and picking it submits the address exactly as written — coverage is not total, so an address the artifact has never heard of is a normal case rather than a refusal.

    The artifact's schema version is now 3. The database serving this was rebuilt from scratch to carry it; a server pointed at a version 2 artifact refuses to start rather than answering the streets lane with an error on every keystroke.

2026-08-21

  • route and snap now say which road you are on. Every route reply names the road each endpoint landed on — from.road / to.road, carrying the OSM way id, the street name, the route number (ref, e.g. "I 70") and the road class (highway, e.g. motorway or residential). The router had always worked this out in order to pick a junction and then thrown it away, so callers who needed a street name went back to search, where a road is indexed by its bounding box: stand at one end of a kilometre of Main Street and its box centre is 500 m away, behind every side street you are not on. Asking the road network instead measures to the road's real shape. Costs nothing extra. Also on routing.snap() and routing.route() in scripts.

  • route(steps: true) returns the road log. The list of roads a trip runs along, in order, each with the distance it begins at and the motorway exit it was reached through:

    US 6  at 2.2 km  |  I 70 at 17.4 km  |  US 40 at 63.6 km (via exit 232)
    

    Consecutive stretches of the same road collapse into one entry, and slip roads fold into the road they lead onto, so a hundred-kilometre drive is a handful of lines rather than a few thousand. It is not turn-by-turn — no "turn left", no lane guidance, no turn angles. It answers "which roads does this drive use", which previously meant sampling the returned polyline every couple of kilometres and running a radius query per sample: 54 queries for one Denver drive, about 40% of which came back with no label. Opt-in, because it costs roughly half a route again.

  • list_areas stops quietly picking between places of the same name. Settlement names repeat — there is a Winter Park in Colorado and another in Florida — and filtering for one used to return whichever ranked highest by place type, with nothing to say the other existed. It now returns resolved_candidates, every place that matched, best first, and a note saying the top one was a guess and on what grounds. New near_lat / near_lon parameters break the tie by distance instead, which is the right answer when the caller knows where they are asking from. The chosen answer has not changed; what changed is that the alternatives are visible.

  • A failed query inside runtime explains itself. Scripts that hit a bad query were interrupted with could not execute query: "<your query>" — the string the caller already had, and no error code. The same query through search reported SYNTAX_INVALID with a hint. The sandbox now uses the server-wide <CODE>: <message> (hint: …) format like everything else. Parse errors also name the character they stopped at and, for the near-misses, the spelling that works: [name~Berthoud] now says the contains operator is =~, instead of failing with "unparsable query" and nothing further.

  • House numbers from OpenAddresses. Address coverage previously came from OpenStreetMap alone. OpenAddresses — an open aggregation of government address registers — is now folded in alongside it, which is roughly eight times the address rows. Sources whose licence is share-alike are refused outright rather than mixed with ODbL data, so a few municipalities are deliberately absent; knowhere://attribution lists what a given database actually contains. Addresses that exist in both sources are collapsed, and the units of one building fold into a single address row carrying its unit list rather than one row per apartment.

  • The FEMA tags that 2026-08-18 announced are actually present. fema:zone_subtype and the eighteen nri:* hazard ratings were documented on 2026-08-18 but never reached a served database — see the correction in that section. They are in this build: on colorado, fema:zone_subtype covers 2,049,847 entries. fema:zone_subtype is what makes zone X readable, separating the 500-year floodplain from genuinely minimal hazard.

  • The g / "geojson" element type is gone. Queries could name a fourth element type alongside node, way and relation, which selected features imported from a GeoJSON file rather than an OpenStreetMap extract. No served area was ever built that way — every one of the 65 areas comes from a PBF extract — so g[...] and {"$type": "geojson"} only ever matched zero rows. Both now fail as an unknown type instead of returning nothing, and * means the three real types. Nothing that returned results before returns fewer.

  • The artifact is about a third smaller, and wide-area search is far faster — but one shape of query got slower. The database was rebuilt around a different spatial index, and the trade is uneven enough to be worth stating in full rather than summarising as a win.

    What got faster is anything covering real ground. Measured on colorado: amenity=cafe within 20 km went from 725 ms to 9.6 ms, a restaurant search within 1 km from 88 ms to 37 ms, within 200 m from 10.2 ms to 6.9 ms. Through the serving path that 20 km query answers in 83 ms, where it used to be slow enough to feel broken.

    What got slower is a tight radius, and most of all a tight radius on a bare key. amenity=restaurant within 30 m went from 0.74 ms to 6.7 ms — a large multiple of a number too small to perceive. But a bare [amenity] within 30 m went from 0.70 ms to 74 ms, and within 1 km from 95 ms to 380 ms; on the serving path those are about 1.8 s and 2.7 s. If you ask for a broad category within a short distance, expect seconds.

    The cause is structural rather than a tuning miss. The old index carried the location inside the text index, so one lookup pruned by tag and by place at once. They are separate now, and when the tag matches a great many features and the radius excludes almost all of them, the text side leads and the location index is reduced to checking its output row by row. Narrowing the tag (amenity=cafe rather than amenity) or widening the radius both avoid it.

    Plain tag lookups with no location at all are about 9% slower. The artifact itself went from 322.6 MB to 208.5 MB raw on wyoming, and from 34.7 GB to 21.9 GB compressed across all 65 areas, so a cold start downloads a good deal less.

  • The synthetic geohash tag is gone from results. Every entry used to carry a geohash key in its tags — not from OpenStreetMap, but written by the build to make spatial queries possible. It was never documented, never usable in a filter, and it is no longer there. tag_keys never listed it and no query could match on it, so nothing that worked before stops working; but anything reading raw tag payloads and expecting that key will not find it.

  • Results in the same rank order may come back in a different sequence. Ranking is unchanged, and so is paging: a query that ranks one result above another still does. What changed is the tiebreak among results that rank equally — the internal id they fall back on is now assigned in geographic order rather than in the order the source file happened to list them. Practically: page one of a broad query like nw[amenity=restaurant](area=colorado) holds a different fifty than it did, all equally good matches.

  • route and walk_reach report a different node_id. The identifier for a snapped junction used to be its OpenStreetMap node id; it is now an internal index into the area's road graph. It has always been an opaque handle — it is only useful for passing back into another routing call in the same area, which still works — but it is no longer an OSM id and should not be treated as one.

  • Travel times are quantised to a tenth of a second per road segment. Route durations are computed from fixed speeds per road class, so they were always estimates with minutes of slack; the stored precision now matches that rather than exceeding it. A reported duration can differ from before by a fraction of a second.

  • A server refuses to start against an artifact it cannot read. Previously a mismatch between the code and the database produced empty answers rather than errors — "nothing nearby" for a place surrounded by things. The artifact now records which generation of the build produced it, and the server checks before serving.

2026-08-19 (later)

  • geocode guesses out loud when a name doesn't quite match. Real names disagree with real queries: OSM writes "Buckley Space Force Base" and you remember "Buckley Air Force Base"; an airport's formal name carries a county yours doesn't. Those lookups used to fail outright. Now, when no name carries every word, the search reruns on the query's most distinctive word and returns the candidates matching the largest share of the rest — every one flagged partial_name: true. The flag is the point: measured against a public corpus of real airport queries across three states, the relaxed pass recovers right answers and wrong ones in roughly a 2-to-1 ratio, so an unflagged partial match would just be a confident mistake. Anything ranking or summarising results should treat a flagged match as a lead to verify. Location checks now warn on it, and location briefs refuse to anchor on one — the same posture as outside_context. Full matches always outrank partial ones, and a query that used to succeed returns exactly what it returned before.

  • Accented names match their unaccented spellings in ranking. "San Jose" and "San José" now count as the same name when geocode decides whether a feature-name match is exact, matching what the search indexes already did.

2026-08-19

  • geocode parses comma-less USPS-shaped addresses. An address pasted from a shipping label or a maps app — 3377 Blake Street STE 113 Denver CO 80205 — carries no commas, and the parser needed them to tell where the street ended. The whole line was read as a street name, the state was never seen, and the query failed with AREA_REQUIRED even though it named one. The street-type word (Street, St, Ave, …) now marks the boundary instead, and a unit (STE 113, APT 4B, # 5) is recognised and set aside rather than searched for. A comma-less address without a street-type word (3377 Broadway Denver CO) still needs its commas: without either there is nothing to say where the street stops and the city starts.

  • geocode finds named places, not just addresses and streets. A point of interest — an airport, a stadium, a park, a supermarket — is not an address and is not a town, and geocode had no layer that could hold one. "Denver International Airport" returned NO_PLACE_NEARBY no matter which area you passed. Worse, when a road happened to share the name, the road answered: "Costco, Denver" resolved to a street called Costco Avenue and reported it as confidently as a correct result. Both were the same missing layer, and it now exists. geocode searches four layers together — house numbers, street names, settlements, and named features — and returns the best. Add a city to pick between branches of a chain: Costco, Denver. Results say which layer answered, in kind: address, street, place or the new poi. Two limits worth knowing. A name needs at least three characters to be searched, because shorter than that the index cannot narrow the search and the query has to read the whole area. And bus stops are excluded: they are named after the street they stand on ("Washington Ave & 16th St"), so leaving them in meant every street query was answered by a bus stop.

  • Punctuation in a name no longer has to match. Names are matched a word at a time rather than as one exact run of characters, so a hyphen you did not type is no longer a dead end. Durango La Plata County Airport finds OSM's Durango-La Plata County Airport; so do Gunnison Crested Butte Regional Airport and Aspen-Pitkin Co/Sardy Field. Measured against a published corpus of real airport names and coordinates (1,435 US airports from OpenFlights, via geocoder-tester), this took colorado from 57.1% of airports answered within their own tolerance to 75.0%, with no change to the city corpus scored alongside it as a control.

  • Duplicate results are folded together. One building can be mapped many times over — the building outline, each entrance, each business inside — and every copy carries the same street address. 1701 Wynkoop St, Denver returned 28 of them, so a single unambiguous building read as an ambiguous answer and filled the whole alternatives list with itself. Candidates that share a name and sit within 100 m of a better-ranked one are now dropped. Places that merely share a name are untouched — two towns' Main Streets stay two answers.

  • geocode errors say what to do about them. Its errors were being explained with advice written for other tools. AREA_REQUIRED told callers to add an (area=...) directive or $area, which is search syntax that geocode does not accept — it takes an area parameter. NO_PLACE_NEARBY told them to check their latitude and longitude order, which they had not supplied. Both now describe geocode, and both point at list_areas(filter: "<place name>") as the way to find which area holds a place when you do not know.

2026-08-18

Correction (2026-08-21). The two FEMA entries in this section — nri:* and fema:zone_subtype — did not reach the serving database on this date and have not reached it since. The artifact published on 2026-08-18 at 07:18 UTC is still the one being served; the overlay configuration that produces both tags landed at 13:42 and 17:11 the same day, hours after that build was cut, and no build was published between then and this correction. Checked against the live server: fema:flood_zone returns 2,171,042 rows for colorado, while fema:zone_subtype, nri:hail and nri:wildfire each return 0. Both tags are carried by the artifact now built and pending deployment; they will be dated here when it is serving.

  • Natural-hazard ratings, as nri:*. FEMA's National Risk Index rates 18 hazards for every US census tract, and the elevated ones now ride along as tags: nri:wildfire, nri:hail, nri:tornado, nri:earthquake, nri:riverine_flood, nri:landslide, nri:heat_wave and eleven more. Two values only — high and very_high, NRI's own "Relatively High" and "Very High" — because a tract carries a rating for all 18 and the quiet ones are most of them. An absent tag means "not elevated, or not rated". NRI writes two kinds of unknown — "No Rating" and "Insufficient Data", 15,507 and 1,061 tracts respectively for wildfire — and those are dropped the same way a "Very Low" is, so absence is not proof of safety. Answers which hazards are worth asking about at all: nw[nri:wildfire=very_high](area=colorado), or read them off a result. Two caveats worth stating. A census tract is coarse — a whole neighbourhood shares one rating — so this screens a question rather than settling it for a building. And the ratings are relative to the rest of the country, so a high in one state is not the same absolute risk as a high in another. NRI's composite "Risk Index" is deliberately not included: it folds social vulnerability and community resilience into the score, so two tracts with identical hazard rate differently by who lives in them.

  • fema:zone_subtype makes zone X readable. A third tag from the FEMA National Flood Hazard Layer, and the one that decides what fema:flood_zone actually means. 76% of the flood layer is zone X, and 76% of that is the 0.2%-annual-chance (500-year) floodplain rather than the "minimal hazard" the bare letter suggests — so X alone pointed the wrong way on the base rate. Call tag_values(area, "fema:zone_subtype") for the vocabulary an area actually holds — 0.2pct and minimal are most of it, with floodway, coastal, shallow and four flavours of levee behind them. A rare subtype is left untagged rather than stamped as a category nobody can query for. floodway is worth knowing separately: it sits inside zone AE and is where building is effectively prohibited. Try nw[fema:zone_subtype=floodway](area=pennsylvania). Insurance requirements still follow fema:sfha, which has not changed.

  • Numeric comparisons compare numbers. Tag values are stored as text, and SQLite orders every text value above every number regardless of what the text says, so nw[bts:noise>=70](area=colorado) returned all 518,832 entries carrying the key — the same answer as >=99999 — while <= matched none and nw[bts:noise=114] found none of the nine entries tag_values reports. > >= < <= (and $gt $gte $lt $lte) now read the number out of the stored value and skip values that do not start with one; = and != against a numeric literal are an exact text match, matching what tag_values shows. Queries written to work around the old behaviour by quoting the value are unaffected.

2026-08-17

  • Transportation noise, as bts:noise. Every place-like entry in a US area now carries the modelled 24-hour average loudness at its location in A-weighted decibels, from the US DOT National Transportation Noise Map (2022/2023): nw[bts:noise](area=colorado). Roughly one entry in five has a value — the model floors at 45 dBA and covers road, rail and air traffic only, so a quiet address has no tag rather than a low number. It is a good relative signal and a poor absolute one: DOT states it "should not be used to evaluate noise levels in individual locations", and the model ignores shielding from terrain and barriers, so it skews high. Canadian areas have none; the source is US-only.

  • attributions records which snapshot of a source an artifact holds. Two new columns: retrieved_at, when this build acquired the source, and version, the vintage where a source declares one (bts-2022). An artifact could previously say that it used OpenStreetMap and FEMA but not which extract or revision, so two builds of the same pipeline could disagree with nothing recording why. version is null for live services, which have no vintage to state.

  • A brief credits the sources its own area was built from. The footer used to carry a hard-coded "Map data © OpenStreetMap contributors (ODbL)", which was true when OpenStreetMap was the only source and would have quietly stopped being the whole truth as soon as another one landed. It now reads the artifact's attributions table for the area the brief covers and names everything that requires a credit. Scoped to the area on purpose: a brief is about one address, and listing every source in the database would be a paragraph of credit for data the document never touched. Briefs rebuilt from a stored input, or generated without a database, fall back to the original sentence.

  • knowhere://docs/legal now describes OpenAddresses as a possible source of house numbers and points at knowhere://attribution for which sources a given database actually contains. No served database carries OpenAddresses data yet.

2026-08-16

  • Attribution, privacy, and terms now ship with the server. New resource knowhere://docs/legal carries the OpenStreetMap/ODbL credit, the privacy policy, and the terms of service from knowhere.live. The ODbL credit and the "these answers measure the data, not the world" caveat also went into the server instructions, so they reach every session without a client having to fetch a resource — a credit nobody reads is not a credit. knowhere://attribution is unchanged and still reports what this particular artifact was built from, per source and per area.

  • around= stops losing features just over a cell boundary. The radius filter narrows the candidate set with a geohash prefix index before it measures anything, and it was choosing a cell one size too small — for an 800 m radius, a cell 610 m tall. Features past that edge were dropped before the distance check ever saw them, with no error and no warning: a flood-zone screen around a Denver address reported nothing within 800 m while a Zone AE feature sat 709 m away. The radius had to be inflated roughly 3.5x before the feature appeared. The cell is now always at least as large as the radius asked for, so around=r and a distance computed on the returned rows agree. bb= picked its cells the same way and had the same silent loss near a box edge; it is fixed by the same change. Wider radii now scan more candidate rows — the distance test is unchanged and still exact, so results only gain the rows that were wrongly missing.

    The largest geohash cell is 45 degrees across, so a bb wider or taller than 90 degrees (or an around radius of a few thousand kilometres) has no cell big enough to hold it. Those queries now skip the geohash step entirely rather than settle for the biggest cell and drop everything outside it — a whole-world bb used to return only what sat near (0°, 0°), losing every entry in the Americas.

  • around= names a bad radius instead of leaking a database error. A radius or centre that is not a finite number (1e309 overflows to infinity, NaN) used to slip past the range checks and reach SQLite as a bare word, which came back as no such column: Inf. It now returns the same "radius must be > 0 metres" / "centre out of range" error as any other bad value.

2026-08-15

  • Multiple bounding boxes work. A query with more than one bb group ((bb=a...,b...)) either errored ("table does not support scanning") or silently applied the other filters to only the first box, because the boxes were OR'd into the WHERE clause without parentheses. The index prune had the same problem in reverse: it required a match in every box at once, so two boxes that do not overlap returned nothing. Both now treat the boxes as a union: an entry matches if it falls in any box, and every other filter still applies.

  • Multi-word values need a tag key. The any-key form ([*=...], [*=~...], and their negations) searches the whole entry rather than one tag, so nothing downstream can check its answer. With a value of two or more words it can no longer tell "Bobs Burgers" from an entry named Bobs whose operator is Burgers, so it now returns an error naming the problem instead of answering with the wrong entries. Single-word values are unaffected, and naming the key ([name="Bobs Burgers"]) always works.

  • Negative filters no longer drop matches by accident. !=, !~, and [!key] used to exclude an entry when the negated key and value appeared anywhere in it, even under different tags — [cuisine!=pizza] dropped a restaurant named "Pizza Hut" with cuisine=italian, and [!cuisine] dropped entries whose only cuisine-ish key was cuisine:vegan. Also fixed: [key!~a,b] now requires the value to contain none of the terms (it could pass on a value containing both), several [*!=...] filters in one query now all apply, and the bare-key forms [!*] and [*>n] — which always errored — plus a negation-only [*!=...] query — which was silently ignored — now return a clear error.

  • FEMA flood data. Every place-like entry in the US areas now carries two tags from the FEMA National Flood Hazard Layer: fema:flood_zone, the zone code (X, AE, A, ...), and fema:sfha, yes or no for whether the entry sits in a Special Flood Hazard Area — the 1%-annual-chance floodplain where flood insurance is typically required. Filter on them like any other tag: nw[fema:sfha=yes](area=texas). Zone geometry is not stored, only the per-entry answer.