TypeScript 61.9%
HTML 37.2%
SQL 0.7%
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