npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rolodexter

v2.12.0

Published

Universal contact field mapper for JavaScript and TypeScript.

Readme

npm version npm downloads node PyPI license MIT

The universal contact field mapper. Route messy, inconsistent contact data from any source into one clean, canonical schema.


Every CRM, email platform, and CSV export invents its own name for the same field. FNAME. firstname. Given Name. Column A. Multiply that by phone formats, address splits, and a decade of ad-hoc exports, and every integration turns into the same hand-written mapping table you already wrote three times this year.

RoloDexter deletes that table. Hand it a payload from anywhere and it gives back your canonical field names, with the values already normalized.

npx rolodexter map contacts.csv --format json

✨ At a glance

Four vendors, one schema. Every row below is real matcher output, not an aspiration:

| Your source calls it | RoloDexter gives you | How it knew | | --- | --- | --- | | FNAME, fname, first | first_name | exact alias | | Given Name | first_name | normalized | | Phone 1 - Value | phone | normalized | | Organization 1 - Name | company | normalized | | Job Titel (typo) | job_title | fuzzy | | adress (typo) | address_line1 | fuzzy | | Column D holding [email protected] | email | value shape | | Column B holding +1 202-555-0143 | phone | value shape |

Those last two are the point: when the header tells you nothing at all, RoloDexter reads the value and routes it anyway.

🤔 Why you'd want it

Reach for RoloDexter when you want:

  • 🎯 One canonical schema across HubSpot, Salesforce, Mailchimp, Google CSV, and whatever your customer emailed you
  • 🧹 Values normalized on the way in: phones to E.164, names to title case, booleans and tags coerced
  • 🕵️ Unlabeled columns rescued by value-shape detection instead of silently dropped
  • 📊 A confidence score per field, so you can gate, audit, or quarantine instead of guessing
  • 🌍 40 languages of alias coverage, pre-generated and cached, never translated mid-request
  • 🧊 Zero config: no schema file, no rules to register, no service to call
  • 🐍 A Python twin sharing the same alias table, for teams straddling both runtimes

📦 Install

npm i rolodexter

…or don't install it at all:

npx rolodexter map contacts.csv

Node >=20. Ships ESM and CommonJS builds plus TypeScript declarations for every entry point.

🚀 Quick start

import { ContactMapper } from "rolodexter";

const result = new ContactMapper().map_payload({
  fname: "jane",
  surname: "doe",
  mobile: "(202) 555-0143",
  employer: "Tech Corp",
  "Column 7": "[email protected]",   // no usable header, matched by value shape
});

result.normalized;
// {
//   first_name: "Jane",
//   last_name:  "Doe",
//   phone:      "+12025550143",
//   company:    "Tech Corp",
//   email:      "[email protected]"
// }

result.match_rate;      // 1
result.matched_count;   // 5
result.unmapped;        // {}

The API is deliberately snake_case in both runtimes, so a mapping written against the Python package reads identically here.

| Option | Default | What it does | | --- | --- | --- | | default_region | "US" | Region used to parse phone numbers with no country code | | normalize | true | Normalize matched values. Set false to keep raw strings | | overrides | none | Pin a header to a field, e.g. { MMERGE6: "company" } | | languages | none | Language codes whose cached alias packs get merged in | | strict | false | Throw NormalizationError on a mapping warning instead of recording it | | confidence_threshold | 0 | Matches scoring below this are demoted to unmapped | | header_cache_max_size | 4096 | LRU cap on cached header matches | | strategies | all four | Replace the matching pipeline outright |

const mapper = new ContactMapper({
  default_region: "GB",
  overrides: { MMERGE6: "company" },
  confidence_threshold: 0.8,
});

mapper.map_payload({ mobile: "020 7946 0958" }).normalized;
// { phone: "+442079460958" }

🧠 How matching works

Every header runs through four strategies in order and stops at the first hit:

| Strategy | Confidence | How it decides | | --- | --- | --- | | exact | 1.0 | The header is a known alias, verbatim | | normalized | 0.95 | It matches after case and format folding (snake, camel, dotted, indexed, vendor-prefixed) | | fuzzy | 0.85 / 0.7 | It scores at least 80 against a known alias. 0.85 above a score of 90, 0.7 below | | heuristic | 0.6 | The header is useless, but the value looks like an email, phone, URL, postal code, or birthday |

mapper.identify("FirstName");                            // first_name     exact      1
mapper.identify("adress");                               // address_line1  fuzzy      0.85
mapper.identify("phne");                                 // phone          fuzzy      0.7
mapper.identify("Column X", { value: "[email protected]" }); // email          heuristic  0.6

What comes back

map_payload returns a MappingResult:

| Member | Type | What it is | | --- | --- | --- | | .normalized | object | Matched fields, keyed by canonical name | | .unmapped | object | Everything it declined to place | | .field_matches | FieldMatch[] | One per input header: original, canonical, confidence, strategy | | .warnings | string[] | Non-fatal issues, thrown instead under strict: true | | .matched_count / .unmatched_count / .match_rate | number | Derived from field_matches | | .get_match(header) | FieldMatch \| null | How one specific header resolved | | .get_all_phones() | string[] | Every phone-shaped value found, deduped and E.164 | | .get_all_emails() | string[] | Every mapped email value, flattened and deduped | | .get_identity_keys() | string[] | Stable email, phone, and scoped source keys for deduplication | | .explain() | string | Human-readable audit of the whole mapping | | .to_dict() | object | JSON-friendly snapshot |

.explain() is the one to reach for when a mapping surprises you:

Mapping: 2 matched, 1 unmatched (match rate 67%)
  'MMERGE6' -> company [exact, conf=1.00]
  'phne'  x unknown [none, conf=0.00]
  'FNAME' -> first_name [exact, conf=1.00]
Warnings:
  ! 'phne': dropped low-confidence match to 'phone' (confidence 0.70 < threshold 0.80)

Beyond a single payload

| Method | What it does | | --- | --- | | map_payload(obj) | Map one record | | map_batch(rows) | Map an array of records | | map_stream(iterable) | Map lazily, for files that will not fit in memory | | profile(iterable, { max_rows }) | Aggregate import-readiness diagnostics in constant memory | | compile_schema(headers) | Resolve a header list once, then reuse it across every row | | map_dataframe(df) | Map a DataFrame-like column store | | identify(header, opts?) | Resolve a single header without mapping anything | | clear_cache() / cache_info() | Inspect and reset the header cache |

map_dataframe() guarantees unique output labels: unmatched source labels are reserved before canonical __N suffixes are assigned, and duplicate input labels are rejected because they cannot be renamed unambiguously. Custom patterns and overrides are validated at construction and raise PatternLoadError with the invalid section.

profile() reports rows and fields seen, aggregate match rate, canonical and unmapped counts, strategy usage, and categorized warnings. It accepts every normal mapping option and can stop at max_rows without consuming the next iterator item:

const profile = mapper.profile(rows, {
  max_rows: 1_000,
  confidence_threshold: 0.8,
});
console.log(profile.to_dict());
console.log(profile.explain());

🎛️ CLI reference

rolodexter map <input>       Map a CSV/JSON/JSONL file to canonical fields
rolodexter explain <header>  Show how a single header resolves
rolodexter fields            List all canonical fields

map accepts:

| Flag | Default | What it does | | --- | --- | --- | | -o, --output <path> | stdout | Where to write results | | --format {auto,csv,json,jsonl} | from -o, else json | Output format | | --in-format {auto,csv,json,jsonl} | from input extension | Input format | | --region <code> | US | Default phone region, ISO-3166 alpha-2 | | --languages <codes> | none | Comma-separated cached language codes | | --strict | off | Fail on any mapping warning | | --min-confidence <0.0-1.0> | 0 | Drop matches below this confidence | | --no-normalize | off | Skip value normalization | | --embedded-phones | off | Also pull phone numbers out of free-text values | | --on-error {fail,skip,quarantine} | fail | How to handle a row that will not parse | | --quarantine-output <path> | <input>.quarantine.jsonl | Where quarantined rows land | | --max-materialized-rows <n> | 100000 | Row cap for JSON/CSV output, 0 disables | | --max-json-input-bytes <n> | 52428800 | Read cap for non-streaming JSON input, 0 disables |

For safety, the quarantine destination must differ from both the input and the mapped output path.

# Mailchimp, Salesforce, Google CSV, and an unnamed column, all in one file
rolodexter map contacts.csv --format json

# Audit as you go, and keep the bad rows instead of dying on them
rolodexter map leads.csv --min-confidence 0.75 --on-error quarantine \
  --quarantine-output leads.bad.jsonl -o leads.clean.json

# Ask how a single header would resolve, before committing to a pipeline
rolodexter explain "Job Titel"
rolodexter explain "Column B" --value "+1 202-555-0143"

# Print every canonical field name
rolodexter fields

🧾 Output

$ rolodexter map contacts.csv --format json
[
  {
    "first_name": "Jane",
    "phone": "+12025550143",
    "company": "Tech Corp",
    "email": "[email protected]"
  },
  {
    "first_name": "Bob",
    "phone": "+16502530000",
    "company": "Acme",
    "email": "[email protected]"
  }
]
Mapped 2 row(s) -> stdout (json)

That input file's headers were FNAME, MobilePhone, Organization 1 - Name, and Column D. No configuration was supplied.

Exit codes

| Code | Meaning | | --- | --- | | 0 | Success, or -h/--help | | 1 | Runtime error: unreadable file, malformed JSON, --min-confidence out of range | | 2 | Usage error: bad, missing, or unrecognized arguments | | 120 | Broken pipe, e.g. piping into head |

🗂️ The canonical schema

62 canonical fields plus an UNKNOWN sentinel, spanning identity, contact, company, address, social, lifecycle, and metadata. rolodexter fields prints the live list; CanonicalField exposes it in code.

| Group | Fields | | --- | --- | | Name | first_name last_name full_name middle_name nickname prefix suffix | | Contact | email phone home_phone work_phone fax whatsapp website | | Company | company job_title department industry company_size | | Address | address_line1 address_line2 city state postal_code country full_address | | Social | linkedin twitter facebook instagram github youtube tiktok discord telegram | | Lifecycle | lead_status lifecycle_stage email_opt_out subscribed verified tags score owner | | Attribution | source source_id source_service referrer_url utm_parameters | | Dates | birthday age created_at updated_at last_contacted | | Commerce | revenue currency | | Freeform | message subject notes metadata | | Profile | gender timezone language_preference |

🌍 Language packs

English ships by default with no extra dependency. The other 40 languages are generated ahead of time into a cache and simply loaded at construction. RoloDexter never calls a translation service on the request path.

rolodexter-i18n --list                # supported languages and cache status
rolodexter-i18n --languages es,fr,de  # generate and cache three packs
const mapper = new ContactMapper({ languages: ["es", "fr"] });

Generating packs needs the optional @vitalets/google-translate-api and unidecode dependencies. Loading cached packs does not, so npm i --omit=optional is still a fully working install for mapping.

🧰 What's in the box

Three entry points, each with its own types:

| Import from | What you get | | --- | --- | | rolodexter | Everything: matcher, normalizers, phone helpers, SUPPORTED_LANGUAGES, generate_language | | rolodexter/core | The matching engine and its tuning constants (EXACT_MATCH_CONFIDENCE, FUZZY_MATCH_THRESHOLD, …), without phone or i18n code | | rolodexter/i18n | Cache and generation helpers behind the rolodexter-i18n CLI |

Runtime dependencies are deliberately few: fuzzball for fuzzy header scoring, libphonenumber-js for phone parsing, csv-stringify for CSV output.

The alias table is not maintained twice. patterns.json lives in the Python package and is synced into this one on every build, so the JavaScript and Python packages cannot drift apart. Both are held to the same golden-corpus conformance fixtures.

🐍 The Python twin

The same engine, alias table, and canonical schema ship as rolodexter on PyPI, with a matching snake_case API:

from rolodexter import ContactMapper

ContactMapper().map_payload({"fname": "jane", "mobile": "(202) 555-0143"}).normalized
# {'first_name': 'Jane', 'phone': '+12025550143'}

📜 Changelog

  • Streaming import profiler: summarize match readiness, unmapped headers, strategy usage, and warnings without materializing the input.
  • Identity helpers: flatten email collisions and generate stable email, phone, and service-scoped source keys.
  • Safer DataFrame and CLI output: prevent ambiguous column labels, quarantine overwrites, and concurrent temporary-file collisions.
  • Validated custom registries: malformed pattern and override data now fails early with actionable errors.
  • Shared header normalizer: the exact, normalized, and fuzzy strategies now fold headers through one helper instead of three copies.
  • Single-source address aliases: the address-prefix list is derived from patterns.json rather than maintained beside it.
  • Validated i18n caches: an invalid cache file now warns and regenerates instead of failing silently.
  • Precomputed fuzzy index: built once per mapper instead of rescanned per header.
  • Shared Py/JS conformance corpus: conformance_cases.json now drives both test suites.

The release that created this package: the full four-strategy pipeline, public normalizers, phone helpers, match strategies, Python-shaped snake_case API, cached i18n loading and generation, batch / stream / schema / DataFrame helpers, ESM and CJS builds, and the rolodexter and rolodexter-i18n CLIs. Shared golden corpora keep it conformant with the Python package.

Full history: CHANGELOG.md

📄 License

MIT © LunarWerx