grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: server-computed locationDisplay on gallery and story views

Adds a formatted locationDisplay string to galleryView and storyView so
clients render one field instead of re-implementing dedup and formatting
logic per platform. Handles POI names ("Blue Bottle Coffee, Oakland,
California, US"), Nominatim city fallbacks ("New York, US"), county-in-
name leakage from older clients ("Kansas City, Missouri, US"), and legacy
hthree records without structured address (preserved as-is).

- lexicons: add locationDisplay to gallery and story view defs
- server/helpers/formatLocation.ts: canonical implementation
- test/formatLocation.test.ts: 11 vitest cases across real-world shapes
- server/hydrate: include locationDisplay in hydrated views
- app/lib/utils/formatLocation.ts: re-export so $lib consumers don't
reach into server/
- app/lib/utils/bsky-post.ts: use shared helper for cross-post line
- UI components: render locationDisplay ?? location.name ?? fallback

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+164 -29
+1 -1
app/lib/components/molecules/GalleryCard.svelte
··· 170 170 {#if gallery.location} 171 171 <!-- svelte-ignore node_invalid_placement_ssr --> 172 172 <a class="location-link" href="/location/{encodeURIComponent(gallery.location.value)}?name={encodeURIComponent(gallery.location.name ?? gallery.location.value)}" onclick={(e) => e.stopPropagation()}> 173 - {gallery.location.name ?? gallery.address?.locality ?? gallery.location.value} 173 + {gallery.locationDisplay ?? gallery.location.name ?? gallery.location.value} 174 174 </a> 175 175 {/if} 176 176 </div>
+1 -1
app/lib/components/organisms/StoryViewer.svelte
··· 361 361 </span> 362 362 {#if currentStory.location} 363 363 <span class="header-location"> 364 - {currentStory.location.name} 364 + {currentStory.locationDisplay ?? currentStory.location.name ?? ''} 365 365 </span> 366 366 {/if} 367 367 </div>
+2 -24
app/lib/utils/bsky-post.ts
··· 1 1 import { callXrpc } from "$hatk/client"; 2 + import { formatStoredLocation } from "$lib/utils/formatLocation"; 2 3 import { parseTextToFacets } from "$lib/utils/rich-text"; 3 4 4 5 interface BskyPostOptions { ··· 26 27 27 28 const graphemeLength = (s: string) => [...new Intl.Segmenter().segment(s)].length; 28 29 29 - // Build location line. `location.name` may be either a POI name 30 - // ("Blue Bottle Coffee") or a Nominatim-formatted fallback that already 31 - // contains locality/state/country ("New York, New York, United States"). 32 - // We take the first comma-separated chunk as the primary label, then 33 - // append locality/region/country while dropping adjacent duplicates — 34 - // this keeps useful context for POIs ("Blue Bottle Coffee, Oakland, California, US") 35 - // while collapsing duplicates for city fallbacks ("New York, US"). 36 - let locationLine: string | null = null; 37 - if (location) { 38 - const trimmedName = location.name.trim(); 39 - const primaryLabel = trimmedName.split(",")[0].trim() || trimmedName; 40 - const parts: string[] = []; 41 - const appendIfDistinct = (value?: string) => { 42 - const v = value?.trim(); 43 - if (!v) return; 44 - if (parts[parts.length - 1]?.toLowerCase() === v.toLowerCase()) return; 45 - parts.push(v); 46 - }; 47 - appendIfDistinct(primaryLabel); 48 - appendIfDistinct(location.address?.locality); 49 - appendIfDistinct(location.address?.region); 50 - appendIfDistinct(location.address?.country); 51 - locationLine = `📍 ${parts.join(", ")}`; 52 - } 30 + const locationLine = location ? `📍 ${formatStoredLocation(location, location.address)}` : null; 53 31 54 32 // Build suffix (location + hashtag + link) 55 33 const suffixLines: string[] = [];
+4
app/lib/utils/formatLocation.ts
··· 1 + // Re-export of the server-owned formatter so client-side callers (UI, cross-post 2 + // builder) import it via `$lib` without reaching into `server/`. The function 3 + // itself is pure and lives in `server/helpers/formatLocation.ts`. 4 + export { formatStoredLocation } from "../../../server/helpers/formatLocation.ts";
+1 -1
app/routes/profile/[did]/story/[rkey]/+page.svelte
··· 71 71 {#if story.location} 72 72 <div class="location"> 73 73 <MapPin size={14} /> 74 - <span>{story.location.name}</span> 74 + <span>{story.locationDisplay ?? story.location.name ?? ''}</span> 75 75 </div> 76 76 {/if} 77 77 {#if !story.expired}
+2 -2
hatk.generated.ts
··· 54 54 const grainDefsLex = {"lexicon":1,"id":"social.grain.defs","defs":{"aspectRatio":{"type":"object","description":"width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit.","required":["width","height"],"properties":{"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}}}}} as const 55 55 const favoriteLex = {"lexicon":1,"id":"social.grain.favorite","defs":{"main":{"type":"record","key":"tid","record":{"type":"object","required":["createdAt","subject"],"properties":{"createdAt":{"type":"string","format":"datetime"},"subject":{"type":"string","format":"at-uri"}}}}}} as const 56 56 const galleryLex = {"lexicon":1,"id":"social.grain.gallery","defs":{"main":{"type":"record","key":"tid","record":{"type":"object","required":["title","createdAt"],"properties":{"title":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":1000},"facets":{"type":"array","description":"Annotations of description text (mentions, URLs, hashtags, etc)","items":{"type":"ref","ref":"app.bsky.richtext.facet"}},"labels":{"type":"union","description":"Self-label values for this post. Effectively content warnings.","refs":["com.atproto.label.defs#selfLabels"]},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"updatedAt":{"type":"string","format":"datetime"},"createdAt":{"type":"string","format":"datetime"}}}}}} as const 57 - const grainGalleryDefsLex = {"lexicon":1,"id":"social.grain.gallery.defs","defs":{"galleryView":{"type":"object","required":["uri","cid","creator","record","indexedAt"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"title":{"type":"string"},"description":{"type":"string"},"cameras":{"type":"array","description":"List of camera make and models used in this gallery derived from EXIF data.","items":{"type":"string"}},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"facets":{"type":"array","description":"Annotations of description text (mentions, URLs, hashtags, etc)","items":{"type":"ref","ref":"app.bsky.richtext.facet"}},"creator":{"type":"ref","ref":"social.grain.actor.defs#profileView"},"record":{"type":"unknown"},"items":{"type":"array","items":{"type":"union","refs":["social.grain.photo.defs#photoView"]}},"favCount":{"type":"integer"},"commentCount":{"type":"integer"},"labels":{"type":"array","items":{"type":"ref","ref":"com.atproto.label.defs#label"}},"createdAt":{"type":"string","format":"datetime"},"indexedAt":{"type":"string","format":"datetime"},"viewer":{"type":"ref","ref":"#viewerState"},"crossPost":{"type":"ref","ref":"#crossPostInfo"}}},"crossPostInfo":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"URL to the cross-posted Bluesky post."}}},"viewerState":{"type":"object","description":"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.","properties":{"fav":{"type":"string","format":"at-uri"}}}}} as const 57 + const grainGalleryDefsLex = {"lexicon":1,"id":"social.grain.gallery.defs","defs":{"galleryView":{"type":"object","required":["uri","cid","creator","record","indexedAt"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"title":{"type":"string"},"description":{"type":"string"},"cameras":{"type":"array","description":"List of camera make and models used in this gallery derived from EXIF data.","items":{"type":"string"}},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"locationDisplay":{"type":"string","description":"Formatted location label for display."},"facets":{"type":"array","description":"Annotations of description text (mentions, URLs, hashtags, etc)","items":{"type":"ref","ref":"app.bsky.richtext.facet"}},"creator":{"type":"ref","ref":"social.grain.actor.defs#profileView"},"record":{"type":"unknown"},"items":{"type":"array","items":{"type":"union","refs":["social.grain.photo.defs#photoView"]}},"favCount":{"type":"integer"},"commentCount":{"type":"integer"},"labels":{"type":"array","items":{"type":"ref","ref":"com.atproto.label.defs#label"}},"createdAt":{"type":"string","format":"datetime"},"indexedAt":{"type":"string","format":"datetime"},"viewer":{"type":"ref","ref":"#viewerState"},"crossPost":{"type":"ref","ref":"#crossPostInfo"}}},"crossPostInfo":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"URL to the cross-posted Bluesky post."}}},"viewerState":{"type":"object","description":"Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests.","properties":{"fav":{"type":"string","format":"at-uri"}}}}} as const 58 58 const itemLex = {"lexicon":1,"id":"social.grain.gallery.item","defs":{"main":{"type":"record","key":"tid","record":{"type":"object","required":["createdAt","gallery","item"],"properties":{"createdAt":{"type":"string","format":"datetime"},"gallery":{"type":"string","format":"at-uri"},"item":{"type":"string","format":"at-uri"},"position":{"type":"integer","default":0}}}}}} as const 59 59 const blockLex = {"lexicon":1,"id":"social.grain.graph.block","defs":{"main":{"key":"tid","type":"record","record":{"type":"object","required":["subject","createdAt"],"properties":{"subject":{"type":"string","format":"did"},"createdAt":{"type":"string","format":"datetime"}}}}}} as const 60 60 const grainGraphFollowLex = {"lexicon":1,"id":"social.grain.graph.follow","defs":{"main":{"key":"tid","type":"record","record":{"type":"object","required":["subject","createdAt"],"properties":{"subject":{"type":"string","format":"did"},"createdAt":{"type":"string","format":"datetime"}}}}}} as const ··· 64 64 const grainPhotoDefsLex = {"lexicon":1,"id":"social.grain.photo.defs","defs":{"photoView":{"type":"object","required":["uri","cid","thumb","fullsize","aspectRatio"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"thumb":{"type":"string","format":"uri","description":"Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View."},"fullsize":{"type":"string","format":"uri","description":"Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View."},"alt":{"type":"string","description":"Alt text description of the image, for accessibility."},"aspectRatio":{"type":"ref","ref":"social.grain.defs#aspectRatio"},"exif":{"type":"ref","ref":"social.grain.photo.defs#exifView","description":"EXIF metadata for the photo, if available."},"gallery":{"type":"ref","ref":"#galleryState"}}},"exifView":{"type":"object","required":["uri","cid","photo","record","createdAt"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"photo":{"type":"string","format":"at-uri"},"record":{"type":"unknown"},"createdAt":{"type":"string","format":"datetime"},"dateTimeOriginal":{"type":"string"},"exposureTime":{"type":"string"},"fNumber":{"type":"string"},"flash":{"type":"string"},"focalLengthIn35mmFormat":{"type":"string"},"iSO":{"type":"integer"},"lensMake":{"type":"string"},"lensModel":{"type":"string"},"make":{"type":"string"},"model":{"type":"string"}}},"galleryState":{"type":"object","required":["item","itemCreatedAt","itemPosition"],"description":"Metadata about the photo's relationship with the subject content. Only has meaningful content when photo is attached to a gallery.","properties":{"item":{"type":"string","format":"at-uri"},"itemCreatedAt":{"type":"string","format":"datetime"},"itemPosition":{"type":"integer"}}}}} as const 65 65 const exifLex = {"lexicon":1,"id":"social.grain.photo.exif","defs":{"main":{"type":"record","description":"Basic EXIF metadata for a photo. Integers are scaled by 1000000 to accommodate decimal values and potentially other tags in the future.","key":"tid","record":{"type":"object","required":["photo","createdAt"],"properties":{"photo":{"type":"string","format":"at-uri"},"createdAt":{"type":"string","format":"datetime"},"dateTimeOriginal":{"type":"string","format":"datetime"},"exposureTime":{"type":"integer"},"fNumber":{"type":"integer"},"flash":{"type":"string"},"focalLengthIn35mmFormat":{"type":"integer"},"iSO":{"type":"integer"},"lensMake":{"type":"string"},"lensModel":{"type":"string"},"make":{"type":"string"},"model":{"type":"string"}}}}}} as const 66 66 const storyLex = {"lexicon":1,"id":"social.grain.story","defs":{"main":{"type":"record","key":"tid","record":{"type":"object","required":["media","aspectRatio","createdAt"],"properties":{"media":{"type":"blob","accept":["image/*","video/*"],"maxSize":5000000},"aspectRatio":{"type":"ref","ref":"social.grain.defs#aspectRatio"},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"labels":{"type":"union","description":"Self-label values for this story. Effectively content warnings.","refs":["com.atproto.label.defs#selfLabels"]},"createdAt":{"type":"string","format":"datetime"}}}}}} as const 67 - const grainStoryDefsLex = {"lexicon":1,"id":"social.grain.story.defs","defs":{"storyView":{"type":"object","required":["uri","cid","creator","thumb","fullsize","aspectRatio","createdAt"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"creator":{"type":"ref","ref":"social.grain.actor.defs#profileView"},"thumb":{"type":"string","format":"uri","description":"Thumbnail URL for the story image."},"fullsize":{"type":"string","format":"uri","description":"Full-size URL for the story image."},"aspectRatio":{"type":"ref","ref":"social.grain.defs#aspectRatio"},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"createdAt":{"type":"string","format":"datetime"},"labels":{"type":"array","items":{"type":"ref","ref":"com.atproto.label.defs#label"}},"expired":{"type":"boolean","description":"Whether the story has passed its 24-hour window."},"commentCount":{"type":"integer"},"viewer":{"type":"ref","ref":"#viewerState"},"crossPost":{"type":"ref","ref":"social.grain.gallery.defs#crossPostInfo"}}},"viewerState":{"type":"object","description":"Metadata about the requesting account's relationship with the story.","properties":{"fav":{"type":"string","format":"at-uri"}}}}} as const 67 + const grainStoryDefsLex = {"lexicon":1,"id":"social.grain.story.defs","defs":{"storyView":{"type":"object","required":["uri","cid","creator","thumb","fullsize","aspectRatio","createdAt"],"properties":{"uri":{"type":"string","format":"at-uri"},"cid":{"type":"string","format":"cid"},"creator":{"type":"ref","ref":"social.grain.actor.defs#profileView"},"thumb":{"type":"string","format":"uri","description":"Thumbnail URL for the story image."},"fullsize":{"type":"string","format":"uri","description":"Full-size URL for the story image."},"aspectRatio":{"type":"ref","ref":"social.grain.defs#aspectRatio"},"location":{"type":"ref","ref":"community.lexicon.location.hthree"},"address":{"type":"ref","ref":"community.lexicon.location.address"},"locationDisplay":{"type":"string","description":"Formatted location label for display."},"createdAt":{"type":"string","format":"datetime"},"labels":{"type":"array","items":{"type":"ref","ref":"com.atproto.label.defs#label"}},"expired":{"type":"boolean","description":"Whether the story has passed its 24-hour window."},"commentCount":{"type":"integer"},"viewer":{"type":"ref","ref":"#viewerState"},"crossPost":{"type":"ref","ref":"social.grain.gallery.defs#crossPostInfo"}}},"viewerState":{"type":"object","description":"Metadata about the requesting account's relationship with the story.","properties":{"fav":{"type":"string","format":"at-uri"}}}}} as const 68 68 const deleteGalleryLex = {"lexicon":1,"id":"social.grain.unspecced.deleteGallery","defs":{"main":{"type":"procedure","description":"Delete a gallery and all associated records (items, photos, EXIF, favorites, comments).","input":{"encoding":"application/json","schema":{"type":"object","required":["rkey"],"properties":{"rkey":{"type":"string","description":"Record key of the gallery to delete."}}}},"output":{"encoding":"application/json","schema":{"type":"object","properties":{}}}}}} as const 69 69 const getActorFavoritesLex = {"lexicon":1,"id":"social.grain.unspecced.getActorFavorites","defs":{"main":{"type":"query","description":"Get galleries favorited by the authenticated actor. Only the actor themselves can view their favorites.","parameters":{"type":"params","required":["actor"],"properties":{"actor":{"type":"string","format":"did"},"limit":{"type":"integer","minimum":1,"maximum":100,"default":30},"cursor":{"type":"string"}}},"output":{"encoding":"application/json","schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"ref","ref":"social.grain.gallery.defs#galleryView"}},"cursor":{"type":"string"}}}}}}} as const 70 70 const getActorProfileLex = {"lexicon":1,"id":"social.grain.unspecced.getActorProfile","defs":{"main":{"type":"query","description":"Get an actor's profile with gallery stats and follow relationships.","parameters":{"type":"params","required":["actor"],"properties":{"actor":{"type":"string","format":"did"},"viewer":{"type":"string","format":"did"}}},"output":{"encoding":"application/json","schema":{"type":"ref","ref":"social.grain.actor.defs#profileViewDetailed"}}}}} as const
+4
lexicons/social/grain/gallery/defs.json
··· 17 17 }, 18 18 "location": { "type": "ref", "ref": "community.lexicon.location.hthree" }, 19 19 "address": { "type": "ref", "ref": "community.lexicon.location.address" }, 20 + "locationDisplay": { 21 + "type": "string", 22 + "description": "Formatted location label for display." 23 + }, 20 24 "facets": { 21 25 "type": "array", 22 26 "description": "Annotations of description text (mentions, URLs, hashtags, etc)",
+4
lexicons/social/grain/story/defs.json
··· 22 22 "aspectRatio": { "type": "ref", "ref": "social.grain.defs#aspectRatio" }, 23 23 "location": { "type": "ref", "ref": "community.lexicon.location.hthree" }, 24 24 "address": { "type": "ref", "ref": "community.lexicon.location.address" }, 25 + "locationDisplay": { 26 + "type": "string", 27 + "description": "Formatted location label for display." 28 + }, 25 29 "createdAt": { "type": "string", "format": "datetime" }, 26 30 "labels": { 27 31 "type": "array",
+41
server/helpers/formatLocation.ts
··· 1 + /** 2 + * Build a display label for a stored location + address pair. 3 + * 4 + * `location.name` may be either a POI name ("Blue Bottle Coffee") or a 5 + * Nominatim-formatted fallback that already contains locality/region/country 6 + * ("New York, New York, United States"). It may also contain a county baked 7 + * in by older clients ("Kansas City, Jackson County, Missouri, United States"). 8 + * 9 + * We take the first comma-separated chunk of the name as the primary label, 10 + * then append locality/region/country while dropping case-insensitive adjacent 11 + * duplicates. This preserves POI context ("Blue Bottle Coffee, Oakland, 12 + * California, US") and collapses redundancy in city fallbacks ("New York, US"). 13 + * 14 + * Legacy records (community.lexicon.location.hthree) without structured 15 + * address use the stored name as-is so we don't strip useful commas. 16 + */ 17 + export function formatStoredLocation( 18 + location: { name?: string | null } | null | undefined, 19 + address: 20 + | { locality?: string | null; region?: string | null; country?: string | null } 21 + | null 22 + | undefined, 23 + ): string { 24 + const name = location?.name?.trim() ?? ""; 25 + const hasAddressContext = !!(address?.locality || address?.region || address?.country); 26 + if (!hasAddressContext) return name; 27 + 28 + const primaryLabel = name.split(",")[0].trim() || name; 29 + const parts: string[] = []; 30 + const appendIfDistinct = (value?: string | null) => { 31 + const v = value?.trim(); 32 + if (!v) return; 33 + if (parts[parts.length - 1]?.toLowerCase() === v.toLowerCase()) return; 34 + parts.push(v); 35 + }; 36 + appendIfDistinct(primaryLabel); 37 + appendIfDistinct(address?.locality); 38 + appendIfDistinct(address?.region); 39 + appendIfDistinct(address?.country); 40 + return parts.join(", "); 41 + }
+2
server/hydrate/galleries.ts
··· 3 3 import type { PhotoView, GalleryView, ExifView } from "$hatk"; 4 4 import type { BaseContext, Row } from "$hatk"; 5 5 import { countComments } from "./comments.ts"; 6 + import { formatStoredLocation } from "../helpers/formatLocation.ts"; 6 7 import { lookupHandles } from "../helpers/lookupHandles.ts"; 7 8 8 9 const SCALE = 1_000_000; ··· 237 238 name: item.value.location.name, 238 239 value: item.value.location.value, 239 240 }, 241 + locationDisplay: formatStoredLocation(item.value.location, item.value.address), 240 242 ...(item.value.address ? { address: item.value.address } : {}), 241 243 } 242 244 : {}),
+2
server/hydrate/stories.ts
··· 2 2 import type { GrainActorProfile, Story, Label, Row, BaseContext } from "$hatk"; 3 3 import { HIDE_LABELS } from "../labels/_hidden.ts"; 4 4 import { countComments } from "./comments.ts"; 5 + import { formatStoredLocation } from "../helpers/formatLocation.ts"; 5 6 import { lookupCrossPosts } from "./galleries.ts"; 6 7 import { lookupHandles } from "../helpers/lookupHandles.ts"; 7 8 ··· 137 138 ...(location 138 139 ? { 139 140 location: { name: location.name, value: location.value }, 141 + locationDisplay: formatStoredLocation(location, address), 140 142 ...(address ? { address } : {}), 141 143 } 142 144 : {}),
+100
test/formatLocation.test.ts
··· 1 + import { describe, expect, test } from "vitest"; 2 + import { formatStoredLocation } from "../server/helpers/formatLocation.ts"; 3 + 4 + describe("formatStoredLocation", () => { 5 + test("POI with locality differs from name — includes full context", () => { 6 + expect( 7 + formatStoredLocation( 8 + { name: "Blue Bottle Coffee" }, 9 + { locality: "Oakland", region: "California", country: "US" }, 10 + ), 11 + ).toBe("Blue Bottle Coffee, Oakland, California, US"); 12 + }); 13 + 14 + test("POI where locality matches name — skips dup", () => { 15 + expect( 16 + formatStoredLocation( 17 + { name: "Manzanita" }, 18 + { locality: "Manzanita", region: "Oregon", country: "US" }, 19 + ), 20 + ).toBe("Manzanita, Oregon, US"); 21 + }); 22 + 23 + test("POI like Overlook with town locality — includes town", () => { 24 + expect( 25 + formatStoredLocation( 26 + { name: "Overlook Mountain Fire Tower" }, 27 + { locality: "Town of Woodstock", region: "New York", country: "US" }, 28 + ), 29 + ).toBe("Overlook Mountain Fire Tower, Town of Woodstock, New York, US"); 30 + }); 31 + 32 + test("Nominatim city fallback — dedupes locality and region matching primary", () => { 33 + expect( 34 + formatStoredLocation( 35 + { name: "New York, New York, United States" }, 36 + { locality: "New York", region: "New York", country: "US" }, 37 + ), 38 + ).toBe("New York, US"); 39 + }); 40 + 41 + test("city fallback with distinct state — keeps state", () => { 42 + expect( 43 + formatStoredLocation( 44 + { name: "Oakland, California, United States" }, 45 + { locality: "Oakland", region: "California", country: "US" }, 46 + ), 47 + ).toBe("Oakland, California, US"); 48 + }); 49 + 50 + test("county baked into name by legacy client — stripped via primary split", () => { 51 + expect( 52 + formatStoredLocation( 53 + { name: "Kansas City, Jackson County, Missouri, United States" }, 54 + { locality: "Kansas City", region: "Missouri", country: "US" }, 55 + ), 56 + ).toBe("Kansas City, Missouri, US"); 57 + }); 58 + 59 + test("deep Nominatim fallback for a street + district + city", () => { 60 + expect( 61 + formatStoredLocation( 62 + { name: "821 Southeast 14th Avenue, Central Eastside, Buckman, Portland, Multnomah County, Oregon, 97214, United States" }, 63 + { locality: "Portland", region: "Oregon", country: "USA" }, 64 + ), 65 + ).toBe("821 Southeast 14th Avenue, Portland, Oregon, USA"); 66 + }); 67 + 68 + test("name already has state abbrev — doesn't duplicate", () => { 69 + expect( 70 + formatStoredLocation( 71 + { name: "Seattle, WA" }, 72 + { locality: "Seattle", region: "WA", country: "US" }, 73 + ), 74 + ).toBe("Seattle, WA, US"); 75 + }); 76 + 77 + test("legacy hthree record with no address — preserves full name", () => { 78 + expect( 79 + formatStoredLocation( 80 + { name: "Eindhoven, North Brabant, Netherlands" }, 81 + null, 82 + ), 83 + ).toBe("Eindhoven, North Brabant, Netherlands"); 84 + }); 85 + 86 + test("empty/null inputs return empty string", () => { 87 + expect(formatStoredLocation(null, null)).toBe(""); 88 + expect(formatStoredLocation({ name: null }, null)).toBe(""); 89 + expect(formatStoredLocation({}, {})).toBe(""); 90 + }); 91 + 92 + test("non-US location with only country — doesn't crash on missing region", () => { 93 + expect( 94 + formatStoredLocation( 95 + { name: "Taipei" }, 96 + { locality: "Taipei", country: "TW" }, 97 + ), 98 + ).toBe("Taipei, TW"); 99 + }); 100 + });