SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
22.0 KB · 345 lines typescript
Raw Blame History
1/**2 * Shared taxonomy mapping for auction-house connectors (Bonhams, Christie's, Sotheby's, Catawiki).3 * Houses describe lots by department/category labels; we map label + title keywords to RareIndex4 * taxonomy slugs (data/taxonomy/categories.json). When nothing matches we return null and the5 * connector decides whether to keep the lot under a family slug or skip it — never invent attributes.6 */78const WATCH_BRANDS: Array<[RegExp, string, string]> = [9  [/\brolex\b/i, 'rolex', 'Rolex'],10  [/\bpatek\b/i, 'patek_philippe', 'Patek Philippe'],11  [/\baudemars\b|\bap royal oak\b/i, 'audemars_piguet', 'Audemars Piguet'],12  [/\bomega\b/i, 'omega', 'Omega'],13  [/\brichard mille\b/i, 'other_watches', 'Richard Mille'],14  [/\bvacheron\b/i, 'other_watches', 'Vacheron Constantin'],15  [/\bcartier\b/i, 'other_watches', 'Cartier'],16  [/\btudor\b/i, 'other_watches', 'Tudor'],17  [/\bbreitling\b/i, 'other_watches', 'Breitling'],18  [/\biwc\b/i, 'other_watches', 'IWC'],19  [/\bjaeger[- ]lecoultre\b|\bjlc\b/i, 'other_watches', 'Jaeger-LeCoultre'],20  [/\blange\b/i, 'other_watches', 'A. Lange & Söhne'],21  [/\bbreguet\b/i, 'other_watches', 'Breguet'],22  [/\bblancpain\b/i, 'other_watches', 'Blancpain'],23  [/\bzenith\b/i, 'other_watches', 'Zenith'],24  [/\bgrand seiko\b/i, 'other_watches', 'Grand Seiko'],25  [/\bseiko\b/i, 'other_watches', 'Seiko'],26  [/\btag heuer\b|\bheuer\b/i, 'other_watches', 'TAG Heuer'],27  [/\bpanerai\b/i, 'other_watches', 'Panerai'],28  [/\bhublot\b/i, 'other_watches', 'Hublot'],29  [/\bf\.?\s?p\.?\s?journe\b/i, 'other_watches', 'F.P. Journe'],30  [/\bmb&f\b/i, 'other_watches', 'MB&F'],31  [/\bde bethune\b/i, 'other_watches', 'De Bethune'],32  [/\bgreubel\b/i, 'other_watches', 'Greubel Forsey'],33  [/\burwerk\b/i, 'other_watches', 'Urwerk'],34  [/\blongines\b/i, 'other_watches', 'Longines'],35  [/\bpiaget\b/i, 'other_watches', 'Piaget'],36  [/\bchopard\b/i, 'other_watches', 'Chopard'],37  [/\bvan cleef\b/i, 'other_watches', 'Van Cleef & Arpels'],38  [/\bbulgari\b|\bbvlgari\b/i, 'other_watches', 'Bulgari'],39  [/\bgirard[- ]perregaux\b/i, 'other_watches', 'Girard-Perregaux'],40  [/\bulysse nardin\b/i, 'other_watches', 'Ulysse Nardin'],41  [/\bfranck muller\b/i, 'other_watches', 'Franck Muller'],42  [/\bbaume\b/i, 'other_watches', 'Baume & Mercier'],43];4445export function watchBrand(title: string): { slug: string; brand: string | null } {46  for (const [re, slug, brand] of WATCH_BRANDS) if (re.test(title)) return { slug, brand };47  return { slug: 'other_watches', brand: null };48}4950/** Rolex-style reference numbers ("Ref. 116500LN", "REF 5711/1A-010", "126610LN"). */51export function watchReference(title: string): string | null {52  const m = title.match(/\b(?:ref(?:erence)?\.?\s*)([0-9]{3,6}[A-Z]{0,4}(?:[\/-][0-9A-Z]{1,6}){0,3})\b/i) ?? title.match(/\b([0-9]{5,6}(?:[A-Z]{1,3}|\/[0-9A-Z]{1,6}))\b/);53  return m ? m[1]!.toUpperCase() : null;54}5556const HANDBAG = /\b(birkin|kelly|constance|hermès|hermes|chanel|louis vuitton|goyard|handbag|clutch|tote|mini bag|shoulder bag|pochette)\b/i;57const SNEAKER = /\b(sneaker|air jordan|nike|yeezy|dunk|adidas|new balance|air max|off-white x)\b/i;58const CARD = /\b(trading card|pokémon|pokemon|charizard|psa \d|bgs \d|cgc \d|topps|panini|upper deck|rookie card|magic: the gathering|yu-gi-oh)/i;59const COMIC = /\b(comic|#\d+\s*\(|amazing spider-man|detective comics|action comics|x-men|batman #|superman #|cgc|cbcs)\b/i;60const COMIC_STRONG = /\b(comic|amazing spider-man|detective comics|action comics|x-men #|batman #|superman #|incredible hulk #|fantastic four #|avengers #|spawn #)/i;61const LEGO = /\blego\b/i;62const FUNKO = /\bfunko\b|\bpop!\b/i;63const VINYL = /\b(vinyl|lp record|acetate|test pressing|78 rpm|shellac)\b/i;64const POSTER = /\b(poster|one[- ]sheet|lobby card|affiche)\b/i;65const GUITAR = /\b(guitar|stratocaster|telecaster|les paul|gibson|fender|bass guitar|synthesizer|moog)\b/i;66const PROP = /\b(screen[- ]used|prop|costume|worn by|production[- ]used|film-used|movie-used)\b/i;67const AUTOGRAPH = /\b(signed|autograph|inscribed|hand-signed)\b/i;68const JERSEY = /\b(jersey|game[- ]worn|match[- ]worn|match[- ]issued|game[- ]used|championship ring|olympic (?:gold|silver|bronze) medal|boxing gloves|signed ball|cricket bat|football boots)\b/i;69const COIN = /\b(coin|sovereign|guinea|denarius|aureus|stater|tetradrachm|solidus|ducat|thaler|dollar 18|morgan|double eagle|mint state|pcgs|ngc|ms6\d|ms7\d|au5\d|proof)\b/i;70const BANKNOTE = /\b(banknote|bank note|treasury note|currency note|specimen note|\bnote\b.*\bbank\b)/i;71const MEDAL = /\b(medal|order of|decoration|campaign group|dso|mc group|victoria cross)\b/i;72const STAMP = /\b(stamp|postal|cover|philatel|penny black|inverted)\b/i;73const WHISKY = /\b(whisky|whiskey|bourbon|macallan|springbank|bowmore|ardbeg|yamazaki|hibiki|karuizawa|glenfiddich|dalmore|laphroaig|brora|port ellen)\b/i;74const COGNAC = /\b(cognac|armagnac|calvados)\b/i;75const RUM = /\b(rum|rhum)\b/i;76const WINE = /\b(bottle|bottles|magnum|jeroboam|imperial|bordeaux|burgundy|champagne|château|chateau|domaine|romanée|romanee|petrus|lafite|latour|margaux|mouton|krug|dom pérignon|dom perignon|barolo|brunello|napa|screaming eagle|rhône|rhone|sauternes|port\b|vintage 19|vintage 20|cases? of \d|\d+\s*bts?\b|\bowc\b|\boc\b)/i;77const FOSSIL = /\b(fossil|ammonite|trilobite|dinosaur|tooth|skull|megalodon|mammoth|skeleton)\b/i;78const METEORITE = /\b(meteorite|pallasite|chondrite|lunar|martian)\b/i;79const MINERAL = /\b(mineral|specimen|quartz|amethyst|tourmaline|fluorite|azurite|malachite|geode|crystal cluster|agate)\b/i;80const MAP = /\b(map|atlas|globe|chart of|plan of)\b/i;81const MANUSCRIPT = /\b(manuscript|letter signed|autograph letter|document signed|typed letter|archive of)\b/i;82const CAMERA = /\b(leica|hasselblad|rolleiflex|camera|lens|nikon f|contax)\b/i;83const CAR = /\b(coupe|coupé|roadster|cabriolet|convertible|saloon|sedan|spyder|spider|berlinetta|gt\b|chassis no|vin\b|ferrari|porsche|lamborghini|bentley|aston martin|jaguar|mercedes-benz|bugatti|maserati|alfa romeo|mclaren)\b/i;84const MOTORCYCLE = /\b(motorcycle|motorbike|ducati|harley|triumph|norton|vincent|brough superior|bsa|moto guzzi|frame no)\b/i;85const AUTOMOBILIA = /\b(mascot|badge|enamel sign|petrol pump|steering wheel|racing helmet|race suit|programme|dashboard|hood ornament)\b/i;86const TOY = /\b(tin toy|tinplate|dinky|corgi|matchbox|hot wheels|teddy|steiff|barbie|action figure|star wars figure|transformers|he-man|g\.i\. joe|playmobil|doll\b)\b/i;87const MODEL_CAR = /\b(1:18|1:43|1:64|1:24|diecast|die-cast|autoart|cmc|minichamps|kyosho)\b/i;88const MODEL_TRAIN = /\b(märklin|marklin|hornby|lionel|bachmann|locomotive|gauge)\b/i;89const VIDEO_GAME = /\b(nintendo|sega|playstation|xbox|game boy|gameboy|atari|sealed game|wata|vga \d|nes\b|snes\b|n64\b|famicom)\b/i;90const JEWEL = /\b(ring|necklace|bracelet|brooch|earrings|pendant|diamond|sapphire|ruby|emerald|carat|cts?\b|tiara|van cleef|graff|harry winston|tiffany)\b/i;91const GEMSTONE = /\b(unmounted|loose diamond|loose stone|gia certified|gia report|rough diamond|natural pearl)\b/i;92const PEN = /\b(fountain pen|montblanc|namiki|pelikan|ballpoint|rollerball)\b/i;93const LIGHTER = /\b(lighter|dunhill|zippo|s\.t\. dupont)\b/i;94const PERFUME = /\b(perfume|parfum|fragrance|eau de)\b/i;95const CLOCK = /\b(clock|chronometer|regulator|longcase|bracket clock|carriage clock)\b/i;96const SILVER = /\b(silver|sterling|silver-gilt|vermeil|tea service|flatware|salver|tankard)\b/i;97const GLASS = /\b(glass|lalique|baccarat|murano|daum|gallé|galle|crystal vase|paperweight)\b/i;98const PORCELAIN = /\b(porcelain|meissen|sèvres|sevres|wedgwood|royal copenhagen|worcester|ceramic|earthenware|stoneware|faience|majolica|delft)\b/i;99const SCI = /\b(microscope|telescope|sextant|astrolabe|orrery|barometer|slide rule|calculating|theodolite|octant)\b/i;100const TYPEWRITER = /\btypewriter\b/i;101const MILITARIA = /\b(helmet|uniform|bayonet|sword|dagger|insignia|regiment|militaria|flintlock|musket|cannon|armour|armor|epaulette)\b/i;102const SPACE = /\b(apollo|nasa|space shuttle|flown|astronaut|cosmonaut|soyuz|gemini mission|mercury mission|lunar module)\b/i;103const AVIATION = /\b(aircraft|aviation|propeller|cockpit|spitfire|concorde|airline)\b/i;104const PHOTO = /\b(gelatin silver|photograph|silver print|c-print|chromogenic|platinum print|daguerreotype|albumen)\b/i;105const CONTEMPORARY = /\b(banksy|kaws|murakami|hirst|koons|kusama|basquiat|haring|warhol|richter|hockney|kapoor|kiefer|condo|nara|stik|invader)\b/i;106const ART = /\b(oil on canvas|oil on panel|acrylic on canvas|watercolour|watercolor|gouache|lithograph|screenprint|etching|engraving|woodcut|sculpture|bronze|drawing|pastel|ink on paper|mixed media|print\b|edition of)\b/i;107const BOOK = /\b(first edition|first printing|edition|volumes|vols?\.|folio|octavo|quarto|bound|binding|signed copy|incunabula|hardback|hardcover|dust[- ]?jacket)\b/i;108const ANIMATION = /\b(animation cel|production cel|celluloid|disney studio|storyboard|concept art)\b/i;109const CHESS = /\b(chess set|chessmen|chess board)\b/i;110const ANTIQUE_FURNITURE = /\b(commode|chair|table|cabinet|bureau|chest of drawers|armchair|mirror|console|settee|sofa|desk|bookcase|sideboard|screen|tapestry|carpet|rug)\b/i;111const DESIGN = /\b(eames|prouvé|prouve|perriand|jeanneret|nakashima|noguchi|wegner|jacobsen|panton|memphis|sottsass|knoll|cassina|vitra|mid-century|scandinavian design)\b/i;112113/** Map a title to a slug given a loose department hint. Returns null when no confident match. */114export function slugFromTitle(title: string, hint: DeptHint = 'unknown'): string | null {115  const t = title;116  // Highly specific signals first (independent of hint)117  if (LEGO.test(t)) return 'lego_sets';118  if (FUNKO.test(t)) return 'funko';119  if (SNEAKER.test(t) && !/\bposter\b/i.test(t)) return 'sneakers';120  if (METEORITE.test(t)) return 'meteorites';121  if (hint === 'watches' || (/\b(wristwatch|watch|chronograph|pocket watch|tourbillon)\b/i.test(t) && hint !== 'jewelry')) {122    if (HANDBAG.test(t) && !/\bwatch|chronograph|wristwatch\b/i.test(t)) return 'luxury_handbags';123    if (PEN.test(t) && !/\bwatch\b/i.test(t)) return 'pens';124    if (LIGHTER.test(t) && !/\bwatch\b/i.test(t)) return 'lighters';125    if (CLOCK.test(t) && !/\bwatch\b/i.test(t)) return 'clocks';126    if (JEWEL.test(t) && !/\bwatch|chronograph|wristwatch\b/i.test(t)) return 'jewelry';127    return watchBrand(t).slug;128  }129  if (hint === 'handbags' || HANDBAG.test(t)) return 'luxury_handbags';130  if (hint === 'jewelry') return GEMSTONE.test(t) ? 'gemstones' : 'jewelry';131  if (hint === 'wine') {132    if (WHISKY.test(t)) return 'whisky';133    if (COGNAC.test(t)) return 'cognac';134    if (RUM.test(t)) return 'rum';135    return 'wine';136  }137  if (WHISKY.test(t)) return 'whisky';138  if (COGNAC.test(t)) return 'cognac';139  if (hint === 'coins') {140    if (BANKNOTE.test(t)) return 'banknotes';141    if (MEDAL.test(t) && !COIN.test(t)) return 'medals';142    return 'coins';143  }144  if (hint === 'stamps' || STAMP.test(t) && hint === 'books') return 'stamps';145  if (hint === 'cars') return MOTORCYCLE.test(t) ? 'motorcycles' : AUTOMOBILIA.test(t) && !CAR.test(t) ? 'automotive_memorabilia' : 'automobiles';146  if (hint === 'motorcycles') return 'motorcycles';147  if (hint === 'automobilia') return 'automotive_memorabilia';148  if (hint === 'comics' || COMIC_STRONG.test(t)) {149    if (ANIMATION.test(t)) return 'animation_art';150    if (/\b(marvel|spider-man|x-men|avengers|hulk|fantastic four|iron man|captain america|thor|wolverine|daredevil)\b/i.test(t)) return 'marvel_comics';151    if (/\b(dc comics|batman|superman|detective comics|action comics|wonder woman|flash|green lantern)\b/i.test(t)) return 'dc_comics';152    if (/\b(manga|tankobon|shonen jump)\b/i.test(t)) return 'manga';153    return 'independent_comics';154  }155  if (hint === 'cards' || CARD.test(t)) {156    if (/pok[eé]mon/i.test(t)) return 'pokemon';157    if (/magic: the gathering|\bmtg\b/i.test(t)) return 'magic_the_gathering';158    if (/yu-gi-oh/i.test(t)) return 'yugioh';159    if (/one piece/i.test(t)) return 'one_piece_card_game';160    if (/lorcana/i.test(t)) return 'disney_lorcana';161    if (/\b(basketball|nba|jordan|lebron|kobe)\b/i.test(t)) return 'basketball_cards';162    if (/\b(baseball|mlb|mantle|ruth|topps 195|bowman 195)\b/i.test(t)) return 'baseball_cards';163    if (/\b(football|nfl|brady|mahomes)\b/i.test(t)) return 'football_cards';164    if (/\b(hockey|nhl|gretzky|mcdavid)\b/i.test(t)) return 'hockey_cards';165    if (/\b(soccer|messi|ronaldo|mbapp|prizm world cup|panini fifa)\b/i.test(t)) return 'soccer_cards';166    if (/\b(f1|formula 1|verstappen|hamilton)\b/i.test(t)) return 'f1_cards';167    return hint === 'cards' ? 'other_tcg' : 'non_sport_cards';168  }169  if (COMIC.test(t) && !CARD.test(t)) return /\b(marvel|spider-man|x-men|avengers|hulk)\b/i.test(t) ? 'marvel_comics' : /\b(batman|superman|dc comics)\b/i.test(t) ? 'dc_comics' : 'independent_comics';170  if (hint === 'popular_culture' || hint === 'entertainment') {171    if (GUITAR.test(t)) return 'musical_instruments';172    if (VINYL.test(t)) return 'music';173    if (POSTER.test(t)) return 'movie_posters';174    if (ANIMATION.test(t)) return 'animation_art';175    if (PROP.test(t)) return 'movie_memorabilia';176    if (VIDEO_GAME.test(t)) return 'video_games';177    if (TOY.test(t)) return 'vintage_toys';178    if (SPACE.test(t)) return 'space';179    if (JERSEY.test(t)) return 'sports_memorabilia';180    if (/\b(beatles|rolling stones|bowie|elvis|hendrix|queen|pink floyd|led zeppelin|tour|concert|gold disc|platinum disc|setlist|lyrics)\b/i.test(t)) return 'music_memorabilia';181    if (/\b(star wars|harry potter|james bond|007|marvel|disney|lord of the rings|batman|indiana jones|jurassic|back to the future)\b/i.test(t)) return 'movie_memorabilia';182    if (AUTOGRAPH.test(t)) return 'autographs';183    return 'movie_memorabilia';184  }185  if (hint === 'sports' || JERSEY.test(t)) return 'sports_memorabilia';186  if (hint === 'toys') {187    if (MODEL_TRAIN.test(t)) return 'model_trains';188    if (MODEL_CAR.test(t)) return 'model_cars';189    if (VIDEO_GAME.test(t)) return 'video_games';190    if (/\b(action figure|hot toys|sideshow|figuarts|nendoroid|mafex)\b/i.test(t)) return 'action_figures';191    if (/\b(doll|barbie|blythe|bisque)\b/i.test(t)) return 'dolls';192    if (/\b(plush|teddy|steiff|jellycat)\b/i.test(t)) return 'plush';193    if (/\b(bearbrick|kaws|medicom|labubu|pop mart|kidrobot)\b/i.test(t)) return 'designer_toys';194    return 'vintage_toys';195  }196  if (hint === 'natural_history') {197    if (FOSSIL.test(t)) return 'fossils';198    if (MINERAL.test(t)) return 'minerals';199    return 'fossils';200  }201  if (hint === 'science') return SCI.test(t) ? 'scientific_instruments' : TYPEWRITER.test(t) ? 'typewriters' : FOSSIL.test(t) ? 'fossils' : MINERAL.test(t) ? 'minerals' : 'scientific_instruments';202  if (hint === 'books') {203    if (MAP.test(t)) return 'maps';204    if (MANUSCRIPT.test(t)) return 'historical_documents';205    if (PHOTO.test(t)) return 'photography';206    return 'books';207  }208  if (hint === 'photographs') return PHOTO.test(t) || !ART.test(t) ? 'photography' : 'art';209  if (hint === 'prints') return CONTEMPORARY.test(t) ? 'contemporary_art' : 'art';210  if (hint === 'contemporary') return 'contemporary_art';211  if (hint === 'art') return CONTEMPORARY.test(t) ? 'contemporary_art' : PHOTO.test(t) ? 'photography' : 'art';212  if (hint === 'clocks') return 'clocks';213  if (hint === 'silver') return 'silver';214  if (hint === 'glass') return 'glass_crystal';215  if (hint === 'ceramics') return 'porcelain';216  if (hint === 'militaria') return MEDAL.test(t) ? 'medals' : 'militaria';217  if (hint === 'design') return 'design_furniture';218  if (hint === 'furniture' || hint === 'decorative') {219    if (CLOCK.test(t)) return 'clocks';220    if (SILVER.test(t)) return 'silver';221    if (GLASS.test(t)) return 'glass_crystal';222    if (PORCELAIN.test(t)) return 'porcelain';223    if (DESIGN.test(t)) return 'design_furniture';224    if (CHESS.test(t)) return 'chess';225    return 'antiques';226  }227  if (hint === 'cameras' || CAMERA.test(t)) return 'cameras';228  if (hint === 'music') return GUITAR.test(t) ? 'musical_instruments' : VINYL.test(t) ? 'music' : 'music_memorabilia';229  if (hint === 'movies') return POSTER.test(t) ? 'movie_posters' : PROP.test(t) ? 'movie_memorabilia' : ANIMATION.test(t) ? 'animation_art' : 'movie_memorabilia';230  if (hint === 'space') return 'space';231  if (hint === 'aviation') return AVIATION.test(t) ? 'aviation' : 'space';232  if (hint === 'perfume') return 'perfume';233  if (hint === 'pens') return PEN.test(t) ? 'pens' : LIGHTER.test(t) ? 'lighters' : 'pens';234  if (hint === 'fashion') return HANDBAG.test(t) ? 'luxury_handbags' : SNEAKER.test(t) ? 'sneakers' : 'fashion_streetwear';235  if (hint === 'video_games') return 'video_games';236  if (hint === 'asian' || hint === 'tribal' || hint === 'antiquities') return 'antiques';237  // No hint: broad keyword sweep238  if (WINE.test(t) && /\b(19|20)\d{2}\b/.test(t)) return 'wine';239  if (COIN.test(t)) return 'coins';240  if (STAMP.test(t)) return 'stamps';241  if (JEWEL.test(t)) return 'jewelry';242  if (CAR.test(t) && /\b(19|20)\d{2}\b/.test(t)) return 'automobiles';243  if (VINYL.test(t)) return 'music';244  if (POSTER.test(t)) return 'movie_posters';245  if (GUITAR.test(t)) return 'musical_instruments';246  if (VIDEO_GAME.test(t)) return 'video_games';247  if (FOSSIL.test(t)) return 'fossils';248  if (MINERAL.test(t)) return 'minerals';249  if (MAP.test(t)) return 'maps';250  if (BOOK.test(t)) return 'books';251  if (PHOTO.test(t)) return 'photography';252  if (CONTEMPORARY.test(t)) return 'contemporary_art';253  if (ART.test(t)) return 'art';254  if (CLOCK.test(t)) return 'clocks';255  if (SILVER.test(t)) return 'silver';256  if (PORCELAIN.test(t)) return 'porcelain';257  if (GLASS.test(t)) return 'glass_crystal';258  if (SCI.test(t)) return 'scientific_instruments';259  if (MILITARIA.test(t)) return 'militaria';260  if (ANTIQUE_FURNITURE.test(t)) return 'antiques';261  return null;262}263264export type DeptHint =265  | 'unknown' | 'watches' | 'handbags' | 'jewelry' | 'wine' | 'coins' | 'stamps' | 'cars' | 'motorcycles' | 'automobilia' | 'cards' | 'comics'266  | 'popular_culture' | 'entertainment' | 'sports' | 'toys' | 'natural_history' | 'science' | 'books' | 'photographs' | 'prints' | 'contemporary' | 'art'267  | 'clocks' | 'silver' | 'glass' | 'ceramics' | 'militaria' | 'design' | 'furniture' | 'decorative' | 'cameras' | 'music' | 'movies' | 'space' | 'aviation'268  | 'perfume' | 'pens' | 'fashion' | 'video_games' | 'asian' | 'tribal' | 'antiquities';269270/** Map a free-text department / category label from any house to a DeptHint. */271export function hintFromLabel(label: string | null | undefined): DeptHint {272  if (!label) return 'unknown';273  const l = label.toLowerCase();274  if (/watch|horolog/.test(l)) return 'watches';275  if (/handbag|luggage/.test(l)) return 'handbags';276  if (/jewel|gem/.test(l)) return 'jewelry';277  if (/wine|whisky|spirit|champagne/.test(l)) return 'wine';278  if (/coin|numismat|banknote|medal/.test(l)) return 'coins';279  if (/stamp|philatel|postal/.test(l)) return 'stamps';280  if (/motor ?car|\bcars\b|automobile|motoring/.test(l)) return 'cars';281  if (/motorcycle/.test(l)) return 'motorcycles';282  if (/automobilia/.test(l)) return 'automobilia';283  if (/trading card|sports card|pokémon|pokemon|tcg/.test(l)) return 'cards';284  if (/comic|animation/.test(l)) return 'comics';285  if (/popular culture|entertainment|rock|pop memorabilia|film memorabilia|music memorabilia/.test(l)) return 'popular_culture';286  if (/sport/.test(l)) return 'sports';287  if (/toy|model|doll|teddy/.test(l)) return 'toys';288  if (/natural history|fossil|mineral|meteorite/.test(l)) return 'natural_history';289  if (/scien|instrument|technology/.test(l)) return 'science';290  if (/book|manuscript|map|atlas|travel|exploration|library/.test(l)) return 'books';291  if (/photograph/.test(l)) return 'photographs';292  if (/print|multiple|edition/.test(l)) return 'prints';293  if (/contemporary|post-war|modern british|20th|21st|street art|urban art/.test(l)) return 'contemporary';294  if (/impressionist|old master|painting|fine art|\bart\b|drawing|sculpture|picture/.test(l)) return 'art';295  if (/clock|barometer/.test(l)) return 'clocks';296  if (/silver|vertu|objects of vertu/.test(l)) return 'silver';297  if (/glass|paperweight/.test(l)) return 'glass';298  if (/ceramic|porcelain|pottery/.test(l)) return 'ceramics';299  if (/arms|armour|armor|militar|medal/.test(l)) return 'militaria';300  if (/design/.test(l)) return 'design';301  if (/furniture|interiors|decorative|works of art|house sale|carpet|rug|tapestr/.test(l)) return 'furniture';302  if (/camera/.test(l)) return 'cameras';303  if (/music|instrument|guitar/.test(l)) return 'music';304  if (/movie|film|cinema|poster/.test(l)) return 'movies';305  if (/space|nasa/.test(l)) return 'space';306  if (/aviation|aeronautic/.test(l)) return 'aviation';307  if (/perfume|fragrance/.test(l)) return 'perfume';308  if (/pen|writing instrument|lighter/.test(l)) return 'pens';309  if (/fashion|sneaker|streetwear|couture|accessor/.test(l)) return 'fashion';310  if (/video ?game/.test(l)) return 'video_games';311  if (/asian|chinese|japanese|islamic|indian|himalayan|korean/.test(l)) return 'asian';312  if (/tribal|african|oceanic|native american|pre-columbian/.test(l)) return 'tribal';313  if (/antiquit|ancient/.test(l)) return 'antiquities';314  return 'unknown';315}316317export function brandFromSlug(slug: string, title: string): string | null {318  if (['rolex', 'patek_philippe', 'audemars_piguet', 'omega', 'other_watches'].includes(slug)) return watchBrand(title).brand;319  if (slug === 'luxury_handbags') {320    const m = title.match(/\b(herm[èe]s|chanel|louis vuitton|dior|gucci|goyard|fendi|bottega veneta|prada|celine|loewe|saint laurent)\b/i);321    return m ? m[1]!.replace(/^herm[èe]s$/i, 'Hermès') : null;322  }323  if (slug === 'lego_sets') return 'LEGO';324  if (slug === 'funko') return 'Funko';325  return null;326}327328/** "PSA 10", "CGC 9.8" etc. are handled by @rareindex/taxonomy; this extracts a LEGO set number. */329export function legoSetNumber(title: string): string | null {330  const m = title.match(/\b(\d{4,5})(?:-1)?\b/);331  return m ? m[1]! : null;332}333334export function isBundleTitle(title: string): boolean {335  return /\b(collection of|group of|lot of|assorted|quantity of|\(\d{2,}\)|\d{2,}\s*(?:items|pieces|cards|bottles|coins)|set of \d{2,}|mixed lot|various)\b/i.test(title);336}337338export function safeYear(title: string): number | null {339  const now = new Date().getUTCFullYear();340  const m = title.match(/\b(1[5-9]\d{2}|20\d{2})\b/);341  if (!m) return null;342  const y = Number(m[1]);343  return y <= now ? y : null;344}345