TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { ConnectorMeta } from '@rareindex/connectors';2import { CertLookupConnector, type PageStatus, type ParsedCert } from '../_g2-grading-lib/cert-base.js';3import { ccgClassify, ccgParse, ccgTrim, pick } from '../_g2-grading-lib/ccg.js';4import { clean, parseNoteGrade, yearOf } from '../_g2-grading-lib/text.js';56/**7 * PMG banknote certificate verification — https://www.pmgnotes.com/certlookup/<cert>/<grade>/ (public).8 * Like NGC, PMG requires the holder grade as the second path segment (1–70 or "NonNumeric"); a URL9 * without it renders the empty lookup form. Seeds: "8067968-054/67".10 */11const PARSER_VERSION = '1.0.0';12const PATTERNS = [/^https?:\/\/(?:www\.)?pmgnotes\.(?:com|uk|de|hk|in|cn)\/(?:[a-z]{2}-[a-z]{2}\/)?certlookup\/([\d-]{5,20})(?:\/([^/?#]+))?\/?(?:[?#].*)?$/i];1314export function splitCert(cert: string): { number: string; grade: string | null } {15 const m = clean(cert).match(/^([\d-]+)(?:[\s/:]+(.+))?$/);16 if (!m) return { number: clean(cert), grade: null };17 return { number: m[1]!, grade: m[2] ? clean(m[2]).replace(/\s+/g, '') : null };18}1920export function certUrl(cert: string): string {21 const { number, grade } = splitCert(cert);22 return `https://www.pmgnotes.com/certlookup/${number}/${grade ? `${encodeURIComponent(grade)}/` : ''}`;23}2425export function certFromUrl(url: string): string | null {26 const m = url.match(PATTERNS[0]!);27 if (!m) return null;28 return m[2] ? `${m[1]}/${decodeURIComponent(m[2])}` : m[1]!;29}3031export class PmgCertConnector extends CertLookupConnector {32 readonly version = '1.0.0';33 readonly parserVersion = PARSER_VERSION;34 readonly grader = 'pmg';35 readonly idKey = 'pmg_cert';36 readonly format = 'html' as const;37 override readonly urlPatterns = PATTERNS;3839 certUrl(cert: string): string {40 return certUrl(cert);41 }42 certFromUrl(url: string): string | null {43 return certFromUrl(url);44 }45 override certIdentifier(cert: string): string {46 return splitCert(cert).number;47 }48 classify(doc: string): PageStatus {49 return ccgClassify(doc);50 }51 trim(doc: string): string {52 return ccgTrim(doc);53 }5455 parse(snapshot: string, info: { cert: string; url: string }): ParsedCert | null {56 const page = ccgParse(snapshot);57 const f = page.fields;58 const cert = pick(f, 'PMG Cert', 'Cert #', 'Cert') ?? splitCert(info.cert).number;59 const description = pick(f, 'Note Description', 'Description');60 if (!description) return null;61 const gradeRaw = pick(f, 'Grade');62 const g = parseNoteGrade(gradeRaw);63 const pickNo = pick(f, 'Note #', 'Pick #', 'Catalog #');64 const region = pick(f, 'Region');65 const identifiers: Record<string, string> = {};66 if (pickNo) identifiers.pick_number = pickNo;67 const countryName = description.split(',')[0]?.trim() || null;68 return {69 title: `${description} PMG ${gradeRaw ?? ''}`.trim(),70 attributes: {71 categorySlug: 'banknotes',72 name: description,73 year: yearOf(description),74 country: countryName,75 region,76 identifiers,77 metadata: {78 pmg_cert: cert,79 serial_number: pick(f, 'Serial #'),80 grade_date: pick(f, 'Grade Date'),81 signatures_vignettes: pick(f, 'Signatures/Vignettes'),82 comments: pick(f, 'Comments'),83 population_url: page.populationUrl,84 fields: f,85 },86 },87 grade: g.grade,88 qualifier: g.qualifier,89 gradeLabel: g.label || null,90 images: page.images,91 population: page.atGrade !== null ? { gradeKey: [g.grade, g.qualifier].filter(Boolean).join(' ') || page.popGradeKey || g.label, atGrade: page.atGrade, higher: page.higher, url: page.populationUrl } : null,92 confidence: pickNo ? 0.93 : 0.85,93 };94 }95}9697export default (meta: ConnectorMeta) => new PmgCertConnector(meta);98