Research note · August 28, 2026
An atlas built from what the records already said.
5,525 places, extracted from museum records' own text and geocoded, not hand-entered. What it took to turn a caption in 19th-century French into a coordinate you can trust, and why the mistakes were more interesting than the successes.
linkedcultureatlasgeocodingdigitalhumanitiesculturalheritageengineering
A record for a 19th-century Bonfils photograph reads "Ruines de la salle Hypostyle." Another, from Francis Frith, says "Nubie. Ibsamboul. Partie septentrionale du Spéos d'Hathor." Neither of those has a latitude or longitude anywhere in the catalog. Both of them know exactly where they are, in the way a caption knows things: implicitly, in period language, and only if you already know what "Ibsamboul" used to be called.
LinkedCulture's Atlas, live now at linkedculture.org/atlas, plots 5,525 of those implicit locations on a real map. Every point on it came from a museum record's own text, not from a curated location field, not from hand-entry. This is what it took to get there, and where it went wrong along the way.
Why a map, not the Galaxy
LinkedCulture already has a geographic-feeling visualization: the Topic Galaxy, a force-directed layout of embedding space, where distance means semantic similarity, not miles. It's the right tool for "what does this collection think is related to what." It is the wrong tool for "where was this photograph taken." A cluster of Egyptian temple photographs sitting near each other in the Galaxy tells you they're about similar things. It doesn't tell you that one was taken at Karnak and the other four hundred kilometers south at Abu Simbel.
The Atlas needed real basemap tiles, real pan and zoom, and real clustering that collapses as you zoom out and expands as you zoom in, because the question it answers is a geographic one, not a semantic one. It's built on vanilla Leaflet with leaflet.markercluster, not a stylized layout borrowing the shape of a map.
Two pilots, one pipeline
The first cohort is 3,156 records from fourteen 19th-century travel photographers already in the index: Bonfils, Frith, Beato, Du Camp, Teynard, Anderson, Sebah, Zangaki, and others. Their portfolios don't stay inside Egypt. Du Camp's 1852 album is titled "Égypte, Nubie, Palestine et Syrie." Anderson's work is mostly Rome. Delamotte photographed the Crystal Palace in London. Filtering the pilot down to "Egypt only" would have thrown away the actual shape of these expeditions, so the pilot kept every location a portfolio touched.
The second cohort came from a smaller observation: Museo Egizio's own catalog already carries a structured provenance field, a comma-separated excavation hierarchy like "Egypt, Luxor / Thebes, Deir el-Medina, Tomb of Kha (TT8)." That's 2,579 records where the place names are curated by the museum's own cataloguers, not free text, so instead of an LLM extraction pass, this cohort got a deterministic parser: walk the hierarchy from most specific to least, match against a set of known Egyptological sites, and fold anything finer-grained (a specific tomb within a necropolis) into a note.
Both cohorts write into the same schema and land in the same table, record_augmentations, under a new method='location_mentions' row. Nothing about museum_records, the authoritative source table, changes. This is the same sidecar pattern the vision pipeline's captions already use: augmentations sit beside the record, never inside it.
Why this needed real historical knowledge, not a local model
Before writing a line of extraction code, the obvious question was whether a small model already running on the server, qwen3:1.7b via Ollama, could do this work without needing an external API call. It couldn't, for a specific reason: this isn't named-entity recognition, it's historical geography.
"Louqsor" is French for Luxor. "Geezeh" is a period spelling of Giza. Those are pattern-matchable. "Ethiopia" is not. One Getty record is titled "The Temple of Amara, Ehtiopia" — in 19th-century European usage, "Ethiopia" (from the classical Greek Aethiopia) meant the lands south of Egypt generally, Nubia and Sudan, not the country that carries the name today. A model, or a person, without that specific piece of period-usage knowledge will place that record roughly two thousand kilometers from where it belongs, with total confidence, because "Ethiopia" is a real country and geocoding it "succeeds." The extraction pass caught this one and flagged it explicitly rather than resolving it silently. A pattern-matcher has no way to know it should be suspicious of a word that isn't wrong, just wrong for the century it's reading.
Across both cohorts combined, 643 records carried a flag like this: a period name, a garbled or ambiguous title, a museum's own uncertain attribution, a reproduction whose depicted subject is a relocated antiquity rather than its original site (several Nubian temples, moved stone by stone to higher ground during the 1960s Aswan High Dam rescue campaign, get pinned to where the photograph shows them, not where they stand in a museum today).
One place, resolved once
603 distinct places came out of the photographer cohort alone. Luxor, Karnak, and Philae recur constantly, once per record that mentions them, and a large language model asked to resolve "Karnak" 400 separate times has no memory between requests. Left alone, that's 400 independent guesses at the same coordinate, each one slightly different, each one a wasted call.
So a new table, location_gazetteer, exists specifically to break that cycle: normalized place name, modern administrative region, coordinates, known raw-text aliases, and where the resolution came from. Every batch of extraction checks it before re-deriving a place it's already seen. It's separate from record_augmentations on purpose. Augmentations are per-record; the gazetteer is per-place, referenced by name, and it persists across pilots. The next cohort that mentions Karnak doesn't re-resolve Karnak. It's already there.
Letting a second source catch the first one's mistakes
Every coordinate an extraction pass proposes is a historically-informed estimate, not a verified one, so it gets checked against Nominatim (OpenStreetMap's geocoder) before it's trusted. Of 656 distinct places, 8 came back with the two sources disagreeing by more than fifty kilometers, and reading through those eight was more useful than the 648 that agreed.
"Egypt" geocoded to Etowah County, Alabama, a town that happens to share the name. "Nile Valley" matched the Nile Valley Hotel, a specific building in Luxor. A record naming the biblical region of "Moab" and another naming "Edom" both resolved to "Jordan Estates," a residential subdivision in Noveleta, Cavite, in the Philippines, because Nominatim's single best text match for a place name plus the word "Jordan" was a subdivision literally named that, ten thousand kilometers from the actual region east of the Dead Sea. "Switzerland" landed on the Chinese Embassy's street address in Bern, an actual point inside the country, just not its center.
None of those are bugs in Nominatim. They're the correct behavior of a text search returning its single best match, and a single best match for a short, common place name is exactly where that kind of search is weakest. Every one of the eight got resolved by hand, in favor of whichever source actually named the right entity, which was the historically-informed estimate in six of the eight cases and Nominatim's own answer in the other two, where its match was legitimate and just a little off-center.
A city is not a temple
The map shipped, and the first real problem showed up within a day of looking at it. Zoom into Asyut, a governorate capital of nearly half a million people, and 181 pins fan out across specific streets, spiderfied apart so they'd be clickable instead of stacked on one pixel. It looked precise. It was fabricated: nothing in any of those 181 records says more than "Asyut." Spreading them across real streets manufactured a level of detail the source text never had.
The instinct to fix that by scattering the pins a little for readability was the wrong instinct, just aimed at the wrong problem. A temple complex like Deir el-Medina really is a bounded place, a few hundred meters across, and nudging a few hundred markers apart there is a reasonable approximation of "found somewhere in this place." A governorate is not a temple. The fix had to distinguish the two, not paper over both the same way.
So every one of the 656 distinct places got classified as a site (a temple, a tomb, a specific building, something with real spatial extent measured in tens or hundreds of meters) or an area (a settlement, region, or country, named with nothing more specific in the source text) — the same kind of historical-geography judgment call the original extraction needed, applied to the places themselves rather than the records. 31 places turned out to be areas, covering 1,565 records: Asyut, Cairo, Rome, Jerusalem, "Egypt" itself, the Dead Sea, the First and Second Cataracts of the Nile.
Getting a second opinion here caught something the first pass missed entirely. Deir el-Medina, a genuine bounded site, has 773 records coincident at one point, more than any area place, including Asyut. The actual problem was never "areas versus sites." It was crowding, and crowding happens at busy sites too. So area places and any site with more than a handful of coincident records now share the same fix: instead of exploding into individual pins, they collapse into one marker, dashed and amber for an area, sized by how many records it holds, that opens a docked panel of the real photographs rather than pretending to know exact positions.

That panel design went through its own bad first draft. The first version was a scrollable list of links, no pictures, and it defeated the entire premise of the project: a museum records dashboard with text where the images should be helps nobody visualize a place they've never been to. The photographs are the point. The panel now shows an actual grid of them, docked to the side rather than covering the map in a modal, because the map itself is still the thing doing the explaining.
Why mapped here
One more gap remained even after the precision was honest: a popup could tell you a record was placed at "El Guisr," but not why. The record's own words that produced that placement were sitting in the pipeline's output the whole time and just weren't making it into the map. The first fix hid them behind a small "?" someone had to notice and click. That was itself the wrong instinct, on a site whose whole premise is showing the work: reasoning that only exists if someone happens to click a question mark is reasoning most people will never see.
So the fix became an always-visible line under every place name: Why mapped here:, no click required. Writing that sentence honestly turned out to be harder than it looked. The obvious version, "the record names this place directly," is only true for 62% of the 5,525 location mentions. For the other 38%, real interpretive work happened: period spellings, French rendered into English, a site identified from what a photograph depicts rather than what its caption literally says. Asserting direct naming for those would have been exactly the kind of fabricated confidence the rest of this project exists to avoid, just moved into a sentence instead of a coordinate.

The sentence is composed once, offline, from three honest states, not generated per record by a model narrating a decision after the fact. When the record's own words literally contain the resolved name, it says so and quotes them. When they don't but the extraction pass left a real note explaining the resolution, that note becomes the reason, minus a handful of notes that turned out to be internal curation flags rather than explanations, filtered out so they don't masquerade as reasoning. When neither applies, the sentence says exactly that: the record reads this, it was matched to that, and the record doesn't use that name. That gap, honestly labeled, was more interesting than a manufactured explanation would have been. When a museum's own catalog carries a "(?)" hedge on a findspot, that gets surfaced too, verbatim, as the museum's own uncertainty rather than something the pipeline quietly resolved past.
Where it landed
- 5,525 location mentions, across 5,517 records, from 656 distinct places, live on a real map at linkedculture.org/atlas.
- Every coordinate traces back to two sources: the record's own text, and a public geocoder cross-check, never a hand-entered guess. Every pin says why, in the open, not behind a click.
- A disagreement between the two sources is a signal, not noise. The eight mismatches this pass caught were more informative than the hundreds of clean agreements, because each one named a specific, fixable reason two reasonable methods landed in different places.
- Historical-terminology traps need a historically-informed reader, not a bigger pattern-matcher. "Ethiopia" isn't a spelling problem. Neither is a temple relief captioned with the pharaoh's biblical name instead of his own.
- Precision has a shape, and a marker should match it. A temple and a governorate are not the same kind of "place," and rendering them identically was its own quiet dishonesty, one that took a second opinion to catch in full.
- A resolved place name is a reusable fact. The gazetteer means the next pilot, in a different collection, in a different language, starts by checking what's already known instead of re-deriving it.
This is a pilot, not the whole collection. It covers two cohorts out of hundreds of thousands of records. What it proves is that the location information was already there, in the text museums have been writing for a hundred and fifty years, waiting for someone to read it the way it was meant to be read.
Explore it: https://linkedculture.org/atlas