#mappers
September 29, 2026 at 8:03 AM
Designing a Location Data Model That Works as a Single Source of Truth
A location single source of truth is one internal record per location, with a strict schema, that every output is generated from: Google Business Profile, other listings, store pages, and structured data. Define it with JSON Schema, validate every change against it plus a few business rules, and write mappers that translate it into each platform's format. _Updated September 2026._ Most multi-location data problems are not API problems. They are "which spreadsheet is right" problems. The website team has one set of hours, the listings team has another, and the store manager changed a third directly in Google. This post shows a data model that makes one record authoritative, with working code for validation and mapping. ## What should a location record contain? Only the facts you publish, plus the IDs that connect the record to each platform. A practical minimum: Field group | Examples | Why it exists ---|---|--- Identity | internal `id`, `store_code`, `name` | Stable keys that never change when the store name does Status | `open`, `temporarily_closed`, `permanently_closed`, `coming_soon` | Drives what every output shows Address and phone | US address parts, E.164 phone | The fields that must match everywhere Hours | regular periods and dated special hours | The most frequently wrong field on any listing Categories | Google category ID | Brand-level decision, stored once External IDs | Google location name, Place ID | Joins your record to each platform Keep an internal `id` separate from the store code. Store codes get reused after closures and renumbered after acquisitions. Your primary key should outlive both. ## How do you define the schema? Use JSON Schema draft 2020-12. It is language-neutral, so the same file can validate records in a Python pipeline, a Node admin tool, and a CI check. { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://example.com/schemas/location.json", "type": "object", "required": ["id", "store_code", "name", "status", "address", "phone", "timezone", "hours"], "additionalProperties": false, "properties": { "id": { "type": "string", "pattern": "^loc_[a-z0-9]+$" }, "store_code": { "type": "string", "minLength": 1 }, "name": { "type": "string", "minLength": 1 }, "status": { "enum": ["open", "temporarily_closed", "permanently_closed", "coming_soon"] }, "address": { "type": "object", "required": ["lines", "city", "state", "postal_code", "country"], "additionalProperties": false, "properties": { "lines": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 2 }, "city": { "type": "string" }, "state": { "type": "string", "pattern": "^[A-Z]{2}$" }, "postal_code": { "type": "string", "pattern": "^[0-9]{5}(-[0-9]{4})?$" }, "country": { "const": "US" } } }, "phone": { "type": "string", "pattern": "^\\+1[0-9]{10}$" }, "website_url": { "type": "string", "format": "uri" }, "timezone": { "type": "string", "examples": ["America/Chicago"] }, "categories": { "type": "object", "properties": { "google_primary": { "type": "string", "pattern": "^gcid:[a-z0-9_]+$" } } }, "hours": { "type": "object", "required": ["regular"], "properties": { "regular": { "type": "array", "items": { "$ref": "#/$defs/period" } }, "special": { "type": "array", "items": { "$ref": "#/$defs/special" } } } }, "external_ids": { "type": "object", "properties": { "google_location": { "type": "string", "pattern": "^locations/[0-9]+$" }, "google_place_id": { "type": "string" } } }, "updated_at": { "type": "string", "format": "date-time" } }, "$defs": { "time": { "type": "string", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$" }, "period": { "type": "object", "required": ["day", "open", "close"], "additionalProperties": false, "properties": { "day": { "enum": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"] }, "open": { "$ref": "#/$defs/time" }, "close": { "$ref": "#/$defs/time" } } }, "special": { "type": "object", "required": ["date", "closed"], "additionalProperties": false, "properties": { "date": { "type": "string", "format": "date" }, "closed": { "type": "boolean" }, "open": { "$ref": "#/$defs/time" }, "close": { "$ref": "#/$defs/time" } }, "if": { "properties": { "closed": { "const": false } } }, "then": { "required": ["open", "close"] } } } } A few choices worth explaining: * **Phone as E.164.** `+15125550142` is unambiguous. Format it for display at the edge, not in storage. * **Times as`HH:MM` strings.** Easy to read and diff. The mappers convert them to each platform's format. * **Special hours require`open` and `close` when `closed` is false.** The `if`/`then` block enforces that. * **`additionalProperties: false`.** Typos like `postal_cod` fail loudly instead of being ignored. The Google category pattern follows the `gcid:...` form that Google uses in the category examples in its location data guide, such as `gcid:software_company`. ## What does a valid record look like? { "id": "loc_014", "store_code": "TX-AUSTIN-014", "name": "Example Hardware", "status": "open", "address": { "lines": ["1200 S Lamar Blvd"], "city": "Austin", "state": "TX", "postal_code": "78704", "country": "US" }, "phone": "+15125550142", "website_url": "https://example.com/locations/texas/austin/south-lamar/", "timezone": "America/Chicago", "hours": { "regular": [ { "day": "MONDAY", "open": "08:00", "close": "20:00" }, { "day": "FRIDAY", "open": "08:00", "close": "20:00" }, { "day": "SATURDAY", "open": "09:00", "close": "18:00" } ], "special": [ { "date": "2026-11-26", "closed": true }, { "date": "2026-12-24", "closed": false, "open": "08:00", "close": "15:00" } ] }, "external_ids": { "google_location": "locations/1234567890123456789" }, "updated_at": "2026-09-20T14:05:00Z" } Short, boring, and reviewable in a pull request. That is the point. ## How do you validate beyond the schema? JSON Schema checks shape. It cannot easily check that two periods on Monday overlap or that last year's holiday is still in the file. Add a small rules layer. # validate.py # pip install jsonschema import json import sys from datetime import date from jsonschema import Draft202012Validator, FormatChecker schema = json.load(open("location.schema.json")) Draft202012Validator.check_schema(schema) validator = Draft202012Validator(schema, format_checker=FormatChecker()) def business_rules(loc: dict) -> list[str]: errors = [] by_day = {} for p in loc["hours"]["regular"]: by_day.setdefault(p["day"], []).append(p) for day, periods in by_day.items(): periods.sort(key=lambda p: p["open"]) for prev, nxt in zip(periods, periods[1:]): if prev["close"] > prev["open"] and nxt["open"] < prev["close"]: errors.append(f"{day}: overlapping periods {prev['open']} and {nxt['open']}") for p in loc["hours"]["regular"]: if p["open"] == p["close"]: errors.append(f"{p['day']}: open equals close, use a 24-hour flag instead") seen = set() for s in loc["hours"].get("special", []): if s["date"] in seen: errors.append(f"{s['date']}: duplicate special-hours date") seen.add(s["date"]) if date.fromisoformat(s["date"]) < date.today(): errors.append(f"{s['date']}: special hours in the past, prune them") if loc["status"] == "permanently_closed" and loc["hours"]["regular"]: errors.append("closed location still has regular hours") return errors def main(path: str) -> int: loc = json.load(open(path)) problems = [f"schema: {e.json_path}: {e.message}" for e in validator.iter_errors(loc)] if not problems: problems = [f"rule: {m}" for m in business_rules(loc)] for p in problems: print(p) return 1 if problems else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1])) Run it in CI on every change to the location files, and in the admin tool before a save. A failing check blocks the change instead of publishing it. Note that the `jsonschema` library only enforces some formats, like `uri`, when optional extra packages are installed. Check the library's format documentation for the formats you rely on. ## How do you map one record to many outputs? Write one small function per destination. Here are two: a Business Information API `Location` body and a schema.org `LocalBusiness` block. # mappers.py DAYS = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"] def _tod(hhmm: str) -> dict: h, m = (int(x) for x in hhmm.split(":")) return {"hours": h, "minutes": m} def _next_day(day: str) -> str: return DAYS[(DAYS.index(day) + 1) % 7] def _date(iso: str) -> dict: y, m, d = (int(x) for x in iso.split("-")) return {"year": y, "month": m, "day": d} def to_gbp(loc: dict) -> dict: """Map an internal record to a Business Information API Location body.""" periods = [] for p in loc["hours"]["regular"]: overnight = p["close"] <= p["open"] periods.append({ "openDay": p["day"], "openTime": _tod(p["open"]), "closeDay": _next_day(p["day"]) if overnight else p["day"], "closeTime": _tod(p["close"]), }) special = [] for s in loc["hours"].get("special", []): item = {"startDate": _date(s["date"]), "closed": s["closed"]} if not s["closed"]: item["openTime"] = _tod(s["open"]) item["closeTime"] = _tod(s["close"]) special.append(item) a = loc["address"] body = { "storeCode": loc["store_code"], "title": loc["name"], "phoneNumbers": {"primaryPhone": loc["phone"]}, "storefrontAddress": { "regionCode": a["country"], "administrativeArea": a["state"], "locality": a["city"], "postalCode": a["postal_code"], "addressLines": a["lines"], }, "regularHours": {"periods": periods}, "specialHours": {"specialHourPeriods": special}, } if "website_url" in loc: body["websiteUri"] = loc["website_url"] return body def to_jsonld(loc: dict, page_url: str) -> dict: """Map the same record to schema.org LocalBusiness JSON-LD.""" spec = [ {"@type": "OpeningHoursSpecification", "dayOfWeek": p["day"].title(), "opens": p["open"], "closes": p["close"]} for p in loc["hours"]["regular"] ] for s in loc["hours"].get("special", []): spec.append({ "@type": "OpeningHoursSpecification", "opens": "00:00" if s["closed"] else s["open"], "closes": "00:00" if s["closed"] else s["close"], "validFrom": s["date"], "validThrough": s["date"], }) a = loc["address"] return { "@context": "https://schema.org", "@type": "LocalBusiness", "@id": f"{page_url}#location", "name": loc["name"], "url": page_url, "telephone": loc["phone"], "address": { "@type": "PostalAddress", "streetAddress": ", ".join(a["lines"]), "addressLocality": a["city"], "addressRegion": a["state"], "postalCode": a["postal_code"], "addressCountry": a["country"], }, "openingHoursSpecification": spec, } The Google mapper follows the Business Information API Location reference: `regularHours.periods` with `openDay`, `openTime`, `closeDay`, and `closeTime`, and `specialHours.specialHourPeriods` with a `startDate` and either times or `closed: true`. Overnight regular hours set `closeDay` to the next day. The JSON-LD mapper follows Google's LocalBusiness structured data guide, which says to mark a business closed all day by setting both `opens` and `closes` to `"00:00"`, and to use `validFrom` and `validThrough` for date-limited hours. This version does not handle overnight special hours. Google's special hours rules require splitting a period that runs past midnight into two sets, so add that before you rely on it for late-night locations. ## How do you push changes safely? Send only what changed. The Business Information API's `locations.patch` takes an `updateMask`, so compute a field-level diff between the last published version and the new record, and patch only those fields. It also accepts `validateOnly=true`, which is useful because Google offers no sandbox. A simple flow: 1. A change lands in the location record through a pull request or admin tool. 2. Schema and rule validation run. 3. A diff job builds the update mask for each destination. 4. Writes go out, with `validateOnly` first for risky fields. 5. A read-back job compares what each platform now serves against the record. ## Who should be allowed to edit which fields? Put ownership in the model, not in people's heads. A common split for brands: * **Brand team:** name, categories, website URL pattern, standard services. * **Location or franchisee:** special hours, local phone, accessibility details, photos. * **Nobody directly on the platform:** anything the pipeline publishes, or the next sync overwrites it. Enforce it with per-field permissions in your admin tool, and with CODEOWNERS rules if the records live in Git. ## Should you build this or use a platform? Build it if you have engineering capacity and many custom outputs. Use a listing platform if the main outputs are listings and pages. Many platforms already keep a location record like this one. Synup, for example, stores one record per location and publishes it to its publisher network, and it offers an API. In that setup, treat the platform record as the source of truth and use a schema like this to validate what you send into it or pull out of it. ## FAQ ### Why JSON Schema instead of a database schema? Use both if you have a database. JSON Schema gives you one portable contract that CI, admin tools, and import scripts can all enforce. ### Should hours be stored in local time or UTC? Local time with an IANA timezone per location. Listings and store pages display local hours, and daylight saving changes make UTC storage error-prone. ### Where should the Google Place ID live? In `external_ids`. Google's Places API policies say place IDs are exempt from caching restrictions, so you can store them indefinitely. ### How do I handle a store that is closing? Change `status`, keep the record, and let each mapper decide what to publish. Deleting the record loses history and external IDs.
dev.to
September 28, 2026 at 9:48 PM
wowie coding javascript
September 28, 2026 at 1:12 PM
Na, all those advances were already concluded weeks if not months ago only not announced yet. There is no russian inheriting any salient right now even by their own mappers admissions.
September 28, 2026 at 10:00 AM
progress halted. His was mostly meant for debugging and stuff, but obviously as CPUs and GPUs have gotten better, full speed with cycle accurate was more doable nowadays.

The main issue is there seems to be mappers aren't 100% supported, so that could cause issues on some games.
September 27, 2026 at 5:58 PM
Half life Extended needs your help! One of the mappers, VortigauntDead, is in the hospital after suffering a stroke. All the information is in the article
www.moddb.com/mods/half-li...
We need your help news - Half-Life: Extended mod for Half-Life
One of our developers suffered from a stroke, and we need your help covering his medical bills through a fundraiser.
www.moddb.com
September 27, 2026 at 2:53 PM
poorly implemented in various emulators and flashcartridge mappers.
September 27, 2026 at 3:15 AM
Reading about NES MMC3 mapper "clones" or sets of mappers that behave similarly is really interesing.

Acclaim, Tengen and other unlisenced developers each were designed with different IRQ timings than that of the official Nintendo's MMC3.

Affected, most notably is the shaking status bar that was
September 27, 2026 at 3:14 AM
I want something I can hold that's worth its weight in gold. I want something that's real. Something real enough to make me feel alive.

Song: Koven - Gold
Mappers: All Poland, OneSpookyboi, ObaMart, Phøenix

#beatsaber #koven #gold @monstercatuncaged.bsky.social
September 26, 2026 at 7:38 AM
Me too, there are so many old established mappers and new mappers. There’s also an amazing variation of styles.
September 26, 2026 at 7:23 AM
We've got some good entries from mappers showcasing their WIPs to our HLDM mapping competition as it comes to an end this October 13!

Don't be shy to post yours too @ LambdaGen's or TWHL's discord!

Authors of the WIPs 🔨
- Jane Thrace
- Rubecula
- Blast4ce
- Captain P
September 25, 2026 at 4:03 PM
Interesting. I believe it was this I went from on that comment:

"We're using both NetRadiant and a special modified version of Trenchbroom. We're so lucky to have SleepwalkR (Trenchbroom developer) on our team, so there are a range of tools the mappers use."

www.indiedb.com/features/int...
Interview: 3D Realms on Wrath: Aeon of Ruin's Modding Scene Heritage feature
We spoke to 3D Realms about the decision to turn to the Quake modding scene for help developing Wrath: Aeon of Ruin.
www.indiedb.com
September 25, 2026 at 3:27 PM
This is so cool! Will look forward to mappers come up with!!!
September 24, 2026 at 5:07 PM
If you're a fan of the Penfield/burnt toast segment, this book is worth a read. Really amazing story of science, friendship and genius. www.penguinrandomhouse.ca/books/726132...
The Mind Mappers by Eric Andrew-Gee | Penguin Random House Canada
NATIONAL BESTSELLERWinner of the 2025 Quebec Writers' Federation Literary Award Concordia University First Book Award and Finalist for the Mavis Gallant Prize for Non-FictionA Globe and Mail Best Book...
www.penguinrandomhouse.ca
September 24, 2026 at 4:42 PM
JSON is live today. Mappers: make cool stuff!
docs.nzp.gay/internal/json
Nazi Zombies: Portable Documentation
Official Mapping and Core documentation for the Quake project Nazi Zombies: Portable
docs.nzp.gay
September 24, 2026 at 3:39 PM
@ohm #recentism is hard to combat. as we succeed in bringing in more historical mappers, we are stuck with a situation that there is more easy recent data. my current focus is on transportation in the 1700s and 1800s in NY and MA and each incremental bit of data is challenging. much of the work […]
Original post on en.osm.town
en.osm.town
September 24, 2026 at 3:33 PM
Huh very interesting. Could try it with an official MBC5 using a flash ROM chip to see if the game is actually MBC5? It's also possible the game had different versions (like regional releases) that used different mappers, but that would be strange to change.
September 24, 2026 at 1:46 PM
#Interstitials are keepers of potentiality over time. In our channels, ideas remain to return when conditions and circumstance support them arising again from our networks. We are not builders and makers. We are tracers and mappers whose maps reveal paths through which step evolution is made.🖖
September 23, 2026 at 11:14 PM
hey can anyone help my friends out? They're an experienced team of programmers, artists, modellers, sound engineers, mappers, writers, and voice actors but what they're really lacking is a bunch of evil soulless fuckers who'll routinely destroy livelihoods to make an extra penny
September 23, 2026 at 7:30 PM
More complex languages are supported too. I made it for CSS modules, because it’s easy. I also implemented a content mapper for MDX, which is more complex. And others are working on content mappers for Astro, Ember, Svelte, Vue, and sorry if I forgot any.
September 23, 2026 at 5:19 PM