extract-human-name 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # extract-human-name
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Extracts, characterizes, and structures human author names from arbitrary text. Tells a person ("Dr. John Doe") from an organization ("The New York Times"), parses a name into its parts, splits multi-author bylines, and formats APA-style citations — no network, no model, no browser.
4
+
5
+ ```bash
6
+ bun add extract-human-name
7
+ ```
8
+
9
+ ```ts
10
+ import { extractHumanName } from "extract-human-name";
11
+
12
+ extractHumanName("by: Dr. John Q. Public Jr.");
13
+ // {
14
+ // author_cite: "Public, John Q., Jr.",
15
+ // author_short: "Public",
16
+ // author_type: 1
17
+ // }
18
+
19
+ extractHumanName("John Doe and Jane Smith");
20
+ // { author_cite: "Doe, John & Smith, Jane", author_short: "Doe & Smith", author_type: 2 }
21
+
22
+ extractHumanName("The New York Times");
23
+ // { author_cite: "The New York Times", author_short: "The New York Times", author_type: 4 }
24
+ ```
25
+
26
+ ## API
27
+
28
+ ### `extractHumanName(author, options?)`
29
+
30
+ The main entry. Sanitizes the input (strips `by:` prefixes, collapses whitespace), splits a byline into individual authors, classifies each one as a person or an organization, and compiles citation forms.
31
+
32
+ | Option | Default | |
33
+ | --- | --- | --- |
34
+ | `formatCiteShortenAuthor` | `false` | Abbreviate given names to initials: `"John Quincy Doe"` → `"Doe, J. Q."` |
35
+ | `maxAuthorsBeforeEtAl` | `2` | List this many authors in full before collapsing to `"First et al."` |
36
+
37
+ `author_type` values:
38
+
39
+ | Value | Meaning |
40
+ | --- | --- |
41
+ | `0` | Empty or unparseable input |
42
+ | `1` | A single person |
43
+ | `2` | Exactly two authors |
44
+ | `3` | Three or more authors |
45
+ | `4` | A single organization |
46
+
47
+ ### `extractHumanNameParts(input)`
48
+
49
+ Parses one name into `{ title, firstname, middle, lastname, honorific }`. Handles `"Last, First"` inversion, multi-word family prefixes (`Ludwig van Beethoven` → lastname `"van Beethoven"`), quoted aliases (`John "The Rocket" Doe`), and fixes the case of all-upper/all-lower input.
50
+
51
+ ### `cleanProfessionalQualifications(authorName)`
52
+
53
+ Strips honorifics, titles, and professional qualifications (`"Dr. John Smith, Professor"` → `"John Smith"`).
54
+
55
+ ### `splitMultipleAuthors(authorString)`
56
+
57
+ Partitions a byline into individual names. Handles `"Last, First & Last, First"`, comma series with a trailing `and`, `&`/`and`/semicolon separators, and drops `et al.`.
58
+
59
+ ### `isOrganization(nameString)`
60
+
61
+ The person-vs-organization heuristic: rigid acronyms (`WHO`), an organization dictionary (decisive terms, or ambiguous terms in pairs like `Morgan Stanley`), qualification counter-terms (`Professor …`), `"Last, First"` inversion, organizational phrases, word count, and the names database.
62
+
63
+ ## The names database: slim default vs. `/full`
64
+
65
+ The default entry is slim: it does **not** bundle the 92k-entry names JSON (`human-names-92k.json`, ~1.1 MB). Person-vs-organization detection runs on a compact built-in list of ~5,000 given names and 5,000 common surnames plus word heuristics. The full database is loaded **only when needed**, one of two ways:
66
+
67
+ ```ts
68
+ // Lazy-load it from the jsDelivr CDN (one shared request, cached;
69
+ // resolves null on failure, so a CDN outage never breaks extraction)
70
+ import { extractHumanName, loadHumanNamesDB } from "extract-human-name";
71
+ await loadHumanNamesDB(); // or { url, fetch } to self-host
72
+
73
+ // Or bundle it: same API, names database registered on import
74
+ import { extractHumanName } from "extract-human-name/full";
75
+ ```
76
+
77
+ `setHumanNamesDB(db)` registers a copy you already hold, and `getHumanNamesDB()` reads the current one. Both entries share one database, so importing `extract-human-name/full` once enables it for the slim import too.
78
+
79
+ With the database loaded, a name whose words are outside the compact list is still recognized as a person:
80
+
81
+ ```ts
82
+ await loadHumanNamesDB();
83
+ extractHumanName("Zephyr Quill Xylo").author_type; // 1 (person), not 4 (organization)
84
+ ```
85
+
86
+ ## Demo
87
+
88
+ The Citation tab of the [extract-pdf site](../extract-pdf/site) (`/demo#cite`) runs name recognition against news, journal, organization and blog URLs.
89
+
90
+ ## Tests
91
+
92
+ ```bash
93
+ cd packages/extract-human-name && bun run test
94
+ ```
95
+
96
+ Covers name parsing, byline splitting, organization detection (with and without the database), the `extractHumanName` citation forms, and the lazy loader (single shared request, failure fallback, retry, `/full` registration).
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Safely partitions a comprehensive author byline into discrete individual names.
3
+ * Applies sophisticated token replacement and pattern recognition to discern standard splits.
4
+ *
5
+ * @param {string} authorString - The source byline string (e.g., "John Doe, Jane Smith, and Alex Jones").
6
+ * @returns {string[]} An array containing individual un-parsed author name segments.
7
+ */
8
+ export function splitMultipleAuthors(authorString: string): string[] {
9
+ if (!authorString) return [];
10
+
11
+ let normalized = authorString.replace(/\s+et\s+al\.?/gi, "");
12
+
13
+ // Pattern A: Last, First & Last, First Formats
14
+ if (/\w+,\s*\w+\s*&\s*\w+,\s*\w+/.test(normalized)) {
15
+ return normalized.split(/\s*&\s*/).filter(Boolean);
16
+ }
17
+
18
+ // Pattern B: Mixed comma + "and" series
19
+ if (
20
+ /\w+,\s*\w+,\s*\w+,\s*\w+/.test(normalized) ||
21
+ /\w+\s\w+,\s\w+\s\w+/.test(normalized)
22
+ ) {
23
+ normalized = normalized.replace(/,\s*(and|&)\s*(?=[^,]*$)/i, " & ");
24
+ return normalized
25
+ .split(/\s*(?:,|&)\s*(?=[^,]*(?:,|$))/)
26
+ .map((s) => s.trim())
27
+ .filter(Boolean);
28
+ }
29
+
30
+ // Pattern C: Standard "and" / "&" conjunction block
31
+ if (/\w+\s\w+\s+(and|&)\s+\w+\s\w+/i.test(normalized)) {
32
+ return normalized
33
+ .split(/\s+(?:and|&)\s+/i)
34
+ .map((s) => s.trim())
35
+ .filter(Boolean);
36
+ }
37
+
38
+ // Fallback broad delimiter standardisation
39
+ normalized = normalized
40
+ .replace(/\s+and\s+/gi, " & ")
41
+ // Break on semi-colons and commas not enclosed by parenthesis blocks (e.g., degrees parsing fallback).
42
+ .replace(/\s*[,;]\s*(?![^(]*\))/g, " & ");
43
+
44
+ return normalized.split(/\s*&\s*/).filter((a) => a.trim().length > 0);
45
+ }