"Local" stops being a one-way door, and the head search stops skipping

Answering Eric's question — no, a local game never looks itself up again
— by making it possible. A local row's Titles line gains "look it up",
reopening it through the SAME cascade resolve uses (board games,
truncation heads, then RPGGeek) rather than the partial re-implementation
reopen_match had; that cascade is now one shared find_candidates()
instead of two drifting copies. Review's manual (f) re-search falls back
to RPGGeek too.

That exposed a real matcher gap: truncation heads jumped from
"drop the last word" straight to "first two words", so a printed title
that buries the real name in the middle was unreachable — "ALICE IS
MISSING A SILENT ROLE PLAYING GAME" never tried "ALICE IS MISSING".
Heads now shrink from the right, longest first (bounded at six, since
each is a rate-limited request); only exact normalized matches count for
heads, so shorter heads cannot match loosely.

Both of Eric's Alice Is Missing rows now find their RPGGeek entries
(311654, and 380459 for Silent Falls) and await his picks in review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-05 23:51:54 -04:00
co-authored by Claude Fable 5
parent 7e95ed607d
commit 50a710ce99
8 changed files with 77 additions and 21 deletions
+3 -3
View File
@@ -23,7 +23,7 @@ FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529
Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,,,version_unknown,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
History of the World,224,History of the World,1991,boardgame,auto,20727,Avalon Hill/Hasbro edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 5948, ""rank"": 1274}, {""bgg_id"": 235591, ""name"": ""History of the World"", ""year"": 2018, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1960, ""rank"": 3203}, {""bgg_id"": 426957, ""name"": ""History of the World"", ""year"": 2025, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 175, ""rank"": 19811}]","[{""version_id"": 20727, ""name"": ""Avalon Hill/Hasbro edition"", ""year"": 2001, ""publishers"": [""Avalon Hill""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,,
Dungeon!,345770,.dungeon,2021,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 345770, ""name"": "".dungeon"", ""year"": 2021, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,,
Dungeon!,,,,,ambiguous,,,,"[{""bgg_id"": 19503, ""name"": ""Dungeon (ICP)"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 77.8, ""owned"": 18, ""rank"": null}]",[],IMG_4502.jpeg,,
TICKET TO RIDE,9209,Ticket to Ride,2004,boardgame,auto,294188,English edition 2012 with Spiel des Jahres logo,version_approved,"[{""bgg_id"": 9209, ""name"": ""Ticket to Ride"", ""year"": 2004, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 468280, ""name"": ""15th anniversary special edition"", ""year"": 2019, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 469076, ""name"": ""Afrikaans/English edition"", ""year"": 2019, ""publishers"": [""Days of Wonder""], ""languages"": [""Afrikaans"", ""English""], ""score"": 3}, {""version_id"": 410646, ""name"": ""English edition 2007 with \""Over 1.1 million games sold\"""", ""year"": 2007, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 191794, ""name"": ""English edition 2007 with \""Over 2 million games sold\"""", ""year"": 2007, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 228833, ""name"": ""English edition 2012 with \""Over 3 million games sold\"""", ""year"": 2012, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 294188, ""name"": ""English edition 2012 with Spiel des Jahres logo"", ""year"": 2012, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 236459, ""name"": ""English edition 2013"", ""year"": 2013, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 447075, ""name"": ""English edition 2015"", ""year"": 2015, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}]",IMG_4504.jpeg,,
CATAN,13,Catan,1995,boardgame,auto,347121,English edition 2016,version_approved,"[{""bgg_id"": 13, ""name"": ""Catan"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 243104, ""rank"": 626}, {""bgg_id"": 66056, ""name"": ""Catan Двубоят"", ""year"": 2010, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 21829, ""rank"": 1187}]","[{""version_id"": 347121, ""name"": ""English edition 2016"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 495726, ""name"": ""English edition 2019"", ""year"": 2019, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 568647, ""name"": ""English edition 2020"", ""year"": 2020, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 733924, ""name"": ""English edition 2023"", ""year"": 2023, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 416798, ""name"": ""Afrikaans edition"", ""year"": 0, ""publishers"": [""Catan Studio""], ""languages"": [""Afrikaans""], ""score"": 2}]",IMG_4504.jpeg;IMG_4542.jpeg,,
RAMPAGE,97903,Rampage,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 97903, ""name"": ""Rampage"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4504.jpeg,,
@@ -114,8 +114,8 @@ MACHI KORO,143884,Machi Koro,2012,boardgame,auto,453051,"English fifth anniversa
Dark Cults: The Horror Story Game,1784,Dark Cults,1983,boardgame,auto,110926,First edition,version_approved,"[{""bgg_id"": 1784, ""name"": ""Dark Cults"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 52.6, ""owned"": null, ""rank"": null}]","[{""version_id"": 110926, ""name"": ""First edition"", ""year"": 1983, ""publishers"": [""Dark House""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 110927, ""name"": ""Second edition"", ""year"": 1985, ""publishers"": [""Dark House""], ""languages"": [""English""], ""score"": 3}]",IMG_4552.jpeg,,
Give Me The Brain,176,Give Me the Brain!,1996,boardgame,auto,27673,Czech edition,version_auto,"[{""bgg_id"": 176, ""name"": ""Give Me the Brain!"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 27673, ""name"": ""Czech edition"", ""year"": 1997, ""publishers"": [""Cheapass Games""], ""languages"": [""Czech""], ""score"": 4}, {""version_id"": 28103, ""name"": ""Cheapass English first paper envelope edition 1996"", ""year"": 1996, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 795469, ""name"": ""Cheapass English paper envelope edition 1998"", ""year"": 1998, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24093, ""name"": ""Cheapass English special edition 2002"", ""year"": 2002, ""publishers"": [""Cheapass Games"", ""James Ernest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 742802, ""name"": ""German zipper bag edition"", ""year"": 2000, ""publishers"": [""Cheapass Games"", ""Truant Spiele""], ""languages"": [""German""], ""score"": 2}]",IMG_4553.jpeg,,
KILL DOCTOR LUCKY,257,Kill Doctor Lucky,1996,boardgame,auto,520364,"Cheapass envelope edition, second printing",version_auto,"[{""bgg_id"": 257, ""name"": ""Kill Doctor Lucky"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 520364, ""name"": ""Cheapass envelope edition, second printing"", ""year"": 1997, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 28629, ""name"": ""Cheapass boxed edition, sixth printing"", ""year"": 2000, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 342532, ""name"": ""Cheapass English 19.5th Anniversary edition"", ""year"": 2016, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 302015, ""name"": ""Cheapass English/German 19.5th Anniversary edition"", ""year"": 2016, ""publishers"": [""Cheapass Games""], ""languages"": [""English"", ""German""], ""score"": 3}, {""version_id"": 24741, ""name"": ""Cheapass envelope edition, first printing"", ""year"": 1996, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 261344, ""name"": ""Cheapass envelope edition, fourth printing"", ""year"": 1999, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133486, ""name"": ""Cheapass print & play edition 2011"", ""year"": 2011, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24760, ""name"": ""Cheapass special edition, eighth printing"", ""year"": 2002, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4555.jpeg,,
ALICE IS MISSING SILENT FALLS EXPANSION,,,,,local,,,,[],[],IMG_4566.jpeg,,
ALICE IS MISSING A SILENT ROLE PLAYING GAME,,,,,local,,,,[],[],IMG_4566.jpeg,,
ALICE IS MISSING SILENT FALLS EXPANSION,,,,,ambiguous,,,,"[{""bgg_id"": 380459, ""name"": ""Alice is Missing: Silent Falls"", ""year"": 2023, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 85.3, ""owned"": 226, ""rank"": null}]",[],IMG_4566.jpeg,,
ALICE IS MISSING A SILENT ROLE PLAYING GAME,,,,,ambiguous,,,,"[{""bgg_id"": 311654, ""name"": ""Alice is Missing"", ""year"": 2020, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 56.1, ""owned"": 2008, ""rank"": null}]",[],IMG_4566.jpeg,,
ETHERFIELDS KITTENBURG EXPANSION,358880,Etherfields: Kittenburg Expansion,2022,boardgame,auto,603694,English edition,version_auto,"[{""bgg_id"": 358880, ""name"": ""Etherfields: Kittenburg Expansion"", ""year"": 2022, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 603694, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 709658, ""name"": ""Polish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 2}, {""version_id"": 699892, ""name"": ""Spanish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}]",IMG_4570.jpeg,,
Parsely,258849,Parsely,2018,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 258849, ""name"": ""Parsely"", ""year"": 2018, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4573.jpeg,,
"...and then, we held hands.",153999,"...and then, we held hands.",2015,boardgame,auto,280845,English edition,version_auto,"[{""bgg_id"": 153999, ""name"": ""...and then, we held hands."", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 280845, ""name"": ""English edition"", ""year"": 2015, ""publishers"": [""LudiCreations""], ""languages"": [""English""], ""score"": 3}]",image.jpg,,
1 title_raw bgg_id bgg_name year type match_status version_id version_name version_status candidates_json version_candidates_json source_photos merged_into dedupe_veto
23 Diplomacy 483 Diplomacy 1959 boardgame auto version_unknown [{"bgg_id": 483, "name": "Diplomacy", "year": 1959, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4502.jpeg
24 Age of Renaissance 26 Age of Renaissance 1996 boardgame auto version_unknown [{"bgg_id": 26, "name": "Age of Renaissance", "year": 1996, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4502.jpeg
25 History of the World 224 History of the World 1991 boardgame auto 20727 Avalon Hill/Hasbro edition version_auto [{"bgg_id": 224, "name": "History of the World", "year": 1991, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 5948, "rank": 1274}, {"bgg_id": 235591, "name": "History of the World", "year": 2018, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 1960, "rank": 3203}, {"bgg_id": 426957, "name": "History of the World", "year": 2025, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 175, "rank": 19811}] [{"version_id": 20727, "name": "Avalon Hill/Hasbro edition", "year": 2001, "publishers": ["Avalon Hill"], "languages": ["English"], "score": 3}] IMG_4502.jpeg
26 Dungeon! 345770 .dungeon 2021 rpgitem auto ambiguous version_unknown [{"bgg_id": 345770, "name": ".dungeon", "year": 2021, "type": "rpgitem", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"bgg_id": 19503, "name": "Dungeon (ICP)", "year": 2001, "type": "boardgame", "exact": true, "fuzzy": 77.8, "owned": 18, "rank": null}] [] IMG_4502.jpeg
27 TICKET TO RIDE 9209 Ticket to Ride 2004 boardgame auto 294188 English edition 2012 with Spiel des Jahres logo version_approved [{"bgg_id": 9209, "name": "Ticket to Ride", "year": 2004, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 468280, "name": "15th anniversary special edition", "year": 2019, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 469076, "name": "Afrikaans/English edition", "year": 2019, "publishers": ["Days of Wonder"], "languages": ["Afrikaans", "English"], "score": 3}, {"version_id": 410646, "name": "English edition 2007 with \"Over 1.1 million games sold\"", "year": 2007, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 191794, "name": "English edition 2007 with \"Over 2 million games sold\"", "year": 2007, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 228833, "name": "English edition 2012 with \"Over 3 million games sold\"", "year": 2012, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 294188, "name": "English edition 2012 with Spiel des Jahres logo", "year": 2012, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 236459, "name": "English edition 2013", "year": 2013, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}, {"version_id": 447075, "name": "English edition 2015", "year": 2015, "publishers": ["Days of Wonder"], "languages": ["English"], "score": 3}] IMG_4504.jpeg
28 CATAN 13 Catan 1995 boardgame auto 347121 English edition 2016 version_approved [{"bgg_id": 13, "name": "Catan", "year": 1995, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 243104, "rank": 626}, {"bgg_id": 66056, "name": "Catan Двубоят", "year": 2010, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": 21829, "rank": 1187}] [{"version_id": 347121, "name": "English edition 2016", "year": 2016, "publishers": ["Catan Studio"], "languages": ["English"], "score": 3}, {"version_id": 495726, "name": "English edition 2019", "year": 2019, "publishers": ["Catan Studio"], "languages": ["English"], "score": 3}, {"version_id": 568647, "name": "English edition 2020", "year": 2020, "publishers": ["Catan Studio"], "languages": ["English"], "score": 3}, {"version_id": 733924, "name": "English edition 2023", "year": 2023, "publishers": ["Catan Studio"], "languages": ["English"], "score": 3}, {"version_id": 416798, "name": "Afrikaans edition", "year": 0, "publishers": ["Catan Studio"], "languages": ["Afrikaans"], "score": 2}] IMG_4504.jpeg;IMG_4542.jpeg
29 RAMPAGE 97903 Rampage 2013 boardgame auto version_unknown [{"bgg_id": 97903, "name": "Rampage", "year": 2013, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4504.jpeg
114 Dark Cults: The Horror Story Game 1784 Dark Cults 1983 boardgame auto 110926 First edition version_approved [{"bgg_id": 1784, "name": "Dark Cults", "year": 1983, "type": "boardgame", "exact": true, "fuzzy": 52.6, "owned": null, "rank": null}] [{"version_id": 110926, "name": "First edition", "year": 1983, "publishers": ["Dark House"], "languages": ["English"], "score": 3}, {"version_id": 110927, "name": "Second edition", "year": 1985, "publishers": ["Dark House"], "languages": ["English"], "score": 3}] IMG_4552.jpeg
115 Give Me The Brain 176 Give Me the Brain! 1996 boardgame auto 27673 Czech edition version_auto [{"bgg_id": 176, "name": "Give Me the Brain!", "year": 1996, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 27673, "name": "Czech edition", "year": 1997, "publishers": ["Cheapass Games"], "languages": ["Czech"], "score": 4}, {"version_id": 28103, "name": "Cheapass English first paper envelope edition 1996", "year": 1996, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 795469, "name": "Cheapass English paper envelope edition 1998", "year": 1998, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 24093, "name": "Cheapass English special edition 2002", "year": 2002, "publishers": ["Cheapass Games", "James Ernest Games"], "languages": ["English"], "score": 3}, {"version_id": 742802, "name": "German zipper bag edition", "year": 2000, "publishers": ["Cheapass Games", "Truant Spiele"], "languages": ["German"], "score": 2}] IMG_4553.jpeg
116 KILL DOCTOR LUCKY 257 Kill Doctor Lucky 1996 boardgame auto 520364 Cheapass envelope edition, second printing version_auto [{"bgg_id": 257, "name": "Kill Doctor Lucky", "year": 1996, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 520364, "name": "Cheapass envelope edition, second printing", "year": 1997, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 5}, {"version_id": 28629, "name": "Cheapass boxed edition, sixth printing", "year": 2000, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 342532, "name": "Cheapass English 19.5th Anniversary edition", "year": 2016, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 302015, "name": "Cheapass English/German 19.5th Anniversary edition", "year": 2016, "publishers": ["Cheapass Games"], "languages": ["English", "German"], "score": 3}, {"version_id": 24741, "name": "Cheapass envelope edition, first printing", "year": 1996, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 261344, "name": "Cheapass envelope edition, fourth printing", "year": 1999, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 133486, "name": "Cheapass print & play edition 2011", "year": 2011, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}, {"version_id": 24760, "name": "Cheapass special edition, eighth printing", "year": 2002, "publishers": ["Cheapass Games"], "languages": ["English"], "score": 3}] IMG_4555.jpeg
117 ALICE IS MISSING SILENT FALLS EXPANSION local ambiguous [] [{"bgg_id": 380459, "name": "Alice is Missing: Silent Falls", "year": 2023, "type": "rpgitem", "exact": true, "fuzzy": 85.3, "owned": 226, "rank": null}] [] IMG_4566.jpeg
118 ALICE IS MISSING A SILENT ROLE PLAYING GAME local ambiguous [] [{"bgg_id": 311654, "name": "Alice is Missing", "year": 2020, "type": "rpgitem", "exact": true, "fuzzy": 56.1, "owned": 2008, "rank": null}] [] IMG_4566.jpeg
119 ETHERFIELDS KITTENBURG EXPANSION 358880 Etherfields: Kittenburg Expansion 2022 boardgame auto 603694 English edition version_auto [{"bgg_id": 358880, "name": "Etherfields: Kittenburg Expansion", "year": 2022, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 603694, "name": "English edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["English"], "score": 3}, {"version_id": 709658, "name": "Polish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Polish"], "score": 2}, {"version_id": 699892, "name": "Spanish edition", "year": 2022, "publishers": ["Awaken Realms"], "languages": ["Spanish"], "score": 2}] IMG_4570.jpeg
120 Parsely 258849 Parsely 2018 rpgitem auto version_unknown [{"bgg_id": 258849, "name": "Parsely", "year": 2018, "type": "rpgitem", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [] IMG_4573.jpeg
121 ...and then, we held hands. 153999 ...and then, we held hands. 2015 boardgame auto 280845 English edition version_auto [{"bgg_id": 153999, "name": "...and then, we held hands.", "year": 2015, "type": "boardgame", "exact": true, "fuzzy": 100.0, "owned": null, "rank": null}] [{"version_id": 280845, "name": "English edition", "year": 2015, "publishers": ["LudiCreations"], "languages": ["English"], "score": 3}] image.jpg
+17 -6
View File
@@ -191,11 +191,13 @@ def _truncation_heads(title_raw: str) -> list[str]:
match = _GAME_WORD.search(title_raw)
if match and match.start() > 0:
heads.append(title_raw[: match.start()])
# last resort: first two words — of the pre-subtitle part, so a title
# like "Blorvath: Quest of the Zzyzx" never yields "Blorvath: Quest"
# last resort: shrink from the right, longest first — a printed title
# can bury the real name in the middle ("ALICE IS MISSING A SILENT ROLE
# PLAYING GAME" is "Alice is Missing"). Only exact normalized matches
# count for heads, so a short head cannot match loosely.
words = (sep_head or title_raw).split()
if len(words) > 2:
heads.append(" ".join(words[:2]))
for size in range(len(words) - 1, 1, -1):
heads.append(" ".join(words[:size]))
seen: set[str] = {normalize_title(title_raw)}
unique: list[str] = []
@@ -204,7 +206,7 @@ def _truncation_heads(title_raw: str) -> list[str]:
if norm and norm not in seen:
seen.add(norm)
unique.append(head)
return unique[:3]
return unique[:6]
def _plausible_candidates(
@@ -449,7 +451,11 @@ def _merged_candidates(
return cands
def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
def find_candidates(client: BGGClient, entry: TitleEntry) -> list[Candidate]:
"""Every search this pipeline knows, in order of confidence. Public
because review's reopen path must search exactly as resolve did — a
partial re-implementation there silently misses whatever the later
steps would have found."""
cands = _merged_candidates(client, entry, entry.title_raw)
if not cands:
# Long transcribed box titles ("CIVILIZATION Game of the Heroic
@@ -472,6 +478,11 @@ def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
)
if cands:
break
return cands
def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow:
cands = find_candidates(client, entry)
row = _classify(client, entry, cands)
if row.match_status == "auto":
resolve_version(client, entry, row)
+7 -2
View File
@@ -32,8 +32,8 @@ from bggpipe.normalize import normalize_title
from bggpipe.resolve import (
MatchRow,
TitleEntry,
_merged_candidates,
_score_version,
find_candidates,
load_titles,
read_matches,
resolve_version,
@@ -391,7 +391,9 @@ class ReviewSession:
title_normalized=normalize_title(row["title_raw"]),
)
try:
cands = _merged_candidates(self.client, entry, row["title_raw"])[:8]
# the FULL cascade resolve uses — board games, truncation heads,
# then RPGGeek — so a row parked as "local" gets the same look
cands = find_candidates(self.client, entry)[:8]
if cands:
stats = {
t.bgg_id: t
@@ -547,6 +549,9 @@ class ReviewSession:
def _research(self, query: str) -> list[dict]:
try:
results = self.client.search(query)
if not results:
# board games first, then RPGGeek — same database, same token
results = self.client.search(query, "rpgitem")
except _BGG_ERRORS as err:
self.console.print(f"[yellow]search unavailable: {err}[/yellow]")
return []
+1 -1
View File
@@ -51,7 +51,7 @@
<p><span class="chip ok">auto</span> matched confidently, no review needed. <span class="chip ok">approved</span> you picked the match yourself.</p>
<p><span class="chip open">ambiguous</span> several plausible games — needs your pick on Review. <span class="chip open">unmatched</span> nothing plausible found — enter a BGG id or re-search on Review.</p>
<p><span class="chip merged">merged</span> two reads judged to be the same physical box; the merge is veto-able on Review. <span class="chip merged">copy</span> one copy of a title you split.</p>
<p><span class="chip open">local — not on BGG</span> you ruled it's a real game BGG doesn't have: it joins the Library from its own photo reads, and never uploads.</p>
<p><span class="chip open">local — not on BGG</span> you ruled it's a real game BGG doesn't have: it joins the Library from its own photo reads (fill in its facts and add a cover photo on its Library page), and never uploads. Changed your mind — or suspect it's on RPGGeek after all? <b>look it up</b> on its Titles row searches again, RPGGeek included.</p>
<p><span class="chip no">rejected</span> you ruled it's a bad read or not worth matching; it stays listed but goes no further.</p>
<p><span class="chip shaky">shaky read</span> the vision model wasn't sure of this transcription and nothing has verified it yet — these are what the Titles badge counts. Clear one by pressing its <b>✓ looks right</b> (the read is fine as-is) or by editing it (you fixed it). A BGG match also clears it: a wrong read wouldn't have matched.</p>
</div>
+19 -6
View File
@@ -89,6 +89,12 @@ function render() {
title="one line, several boxes? make each photo its own copy">
split into copies</button>`
: ""}
${c.status === "local"
? `<button class="wrongmatch" data-title="${esc(c.title_raw)}"
data-photos="${esc(c.photos.join(";"))}" data-rowix="${c.row_ix ?? ""}"
title="search BGG and RPGGeek again — RPGs and expansions often are listed">
look it up</button>`
: ""}
${c.version_status === "version_ambiguous"
? `<a class="ballotlink" href="/review#editions"
title="this copy's edition ballot is open">ballot in Review →</a>`
@@ -124,15 +130,22 @@ document.getElementById("catbody").addEventListener("click", async e => {
if (cancel) { EDITING = null; GATE.reset(); render(); refresh().catch(() => {}); return; }
const wm = e.target.closest("button.wrongmatch");
if (wm) {
// in the edit panel it means "wrong match"; on a local row it means
// "look it up" — same reopen, different starting point
const f = wm.closest("form.editform");
if (!confirm(`"${f.dataset.title}" matched the wrong game? This clears the ` +
`match and sends it back to Review for a re-search.`)) return;
const src = f ? f.dataset : wm.dataset;
if (f && !confirm(`"${src.title}" matched the wrong game? This clears the ` +
`match and sends it back to Review for a re-search.`)) return;
const res = await apiPost("/api/reopen-match", {
title_raw: f.dataset.title,
source_photos: f.dataset.photos,
row_ix: f.dataset.rowix === "" ? null : Number(f.dataset.rowix),
title_raw: src.title,
source_photos: src.photos,
row_ix: src.rowix === "" ? null : Number(src.rowix),
});
if (res) { EDITING = null; GATE.reset(); refresh().catch(() => {}); }
if (res) {
EDITING = null; GATE.reset(); refresh().catch(() => {});
showToast(`searching BGG and RPGGeek for <b>${esc(src.title)}</b> —
<a href="/review#matches">pick a match in Review</a>`);
}
return;
}
const pe = e.target.closest("button.pickedition");
+4 -2
View File
@@ -1350,8 +1350,10 @@ def create_app(
revision["n"] += 1
_refuse_if_rewriting()
row = find_row(body.title_raw, body.source_photos, body.row_ix)
if row["match_status"] not in RECOGNIZED_MATCH_STATUSES:
raise HTTPException(400, "only a matched row can be reopened")
# "local" reopens too: it means "BGG doesn't have this", and the
# re-search now covers RPGGeek, where such a game may well be
if row["match_status"] not in (*RECOGNIZED_MATCH_STATUSES, "local"):
raise HTTPException(400, "only a matched or local row can be reopened")
session.reopen_match(row)
return state()
+2 -1
View File
@@ -213,7 +213,8 @@ def test_truncation_heads_order_and_dedupe():
heads = _truncation_heads(CIV_TITLE)
assert heads[0] == "CIVILIZATION Game of the Heroic Age" # before separator
assert heads[1] == "CIVILIZATION" # before the "Game ..." descriptor
assert len(heads) <= 3
assert len(heads) <= 6 # bounded: each head is a rate-limited request
assert "CIVILIZATION Game" in heads # shrinks from the right, longest first
def test_truncation_heads_game_word_without_separator():
+24
View File
@@ -647,3 +647,27 @@ def test_reopen_match_returns_a_fresh_ballot(tmp_path):
dead.reopen_match(row2)
assert row2["match_status"] == "unmatched"
assert dead.warnings # the degradation is visible
def test_local_rows_can_be_looked_up_again_on_rpggeek(tmp_path):
""" "Local" means BGG has no board game by that name — but RPGGeek,
the same database under type=rpgitem, often does. A local row must not
be a one-way door."""
cfg = _setup(
tmp_path,
[
_row(
title_raw="ALICE IS MISSING: A SILENT ROLE PLAYING GAME",
match_status="local",
)
],
)
session = ReviewSession(
cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client()
)
row = session.rows[0]
session.reopen_match(row)
assert row["match_status"] == "ambiguous"
candidates = json.loads(row["candidates_json"])
assert any(c["type"] == "rpgitem" for c in candidates)
assert any("Alice is Missing" in c["name"] for c in candidates)