← back to Trademarks Copyright

src/lib/scoring.ts

66 lines

import type { Item } from "./types";

export interface ScoringInputs {
  distinctiveness_score: number | null;
  recognition_score: number | null;
  domain_available: boolean | null;
  competing_active_marks: number | null;
  monetization_breadth: number | null;
  expired_date: string | Date;
}

/**
 * Composite 0-100. Weights:
 *   distinctiveness 25, years-since-expired 20, recognition 15,
 *   domain 15, competing-marks 15, monetization 10.
 * Sweet spot for "years since expired" is 3-15. Too recent = revival risk.
 * Too old = brand memory decayed.
 */
export function scoreItem(inp: ScoringInputs): number {
  const distinct = clamp(inp.distinctiveness_score ?? 50, 0, 100);
  const recog = clamp(inp.recognition_score ?? 30, 0, 100);
  const monet = clamp(inp.monetization_breadth ?? 40, 0, 100);

  const yearsExpired = yearsSince(inp.expired_date);
  const ageScore = ageFit(yearsExpired);

  const domainScore = inp.domain_available === true ? 100 : inp.domain_available === false ? 15 : 50;

  // Competing active marks penalty: 0 = perfect, 10+ = heavy saturation
  const competing = inp.competing_active_marks ?? 0;
  const competingScore = clamp(100 - competing * 10, 0, 100);

  const composite =
    distinct * 0.25 +
    ageScore * 0.20 +
    recog * 0.15 +
    domainScore * 0.15 +
    competingScore * 0.15 +
    monet * 0.10;

  return round2(composite);
}

function yearsSince(d: string | Date): number {
  const past = new Date(d).getTime();
  const now = Date.now();
  return (now - past) / (365.25 * 24 * 3600 * 1000);
}

function ageFit(years: number): number {
  if (years < 0) return 0;
  if (years < 1) return 20;   // too recent — revival likely
  if (years < 3) return 55;
  if (years <= 15) return 100; // sweet spot
  if (years <= 30) return 70;
  if (years <= 50) return 45;
  return 30;
}

const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
const round2 = (n: number) => Math.round(n * 100) / 100;

export function yearsExpired(item: Pick<Item, "expired_date">): number {
  return round2(yearsSince(item.expired_date));
}