Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to this project will be documented here.

## [1.1.0] - 2026-10-20
- Default `cattax` is now `"9"` (IAB Content Taxonomy 3.1) in the Python pipeline, CLI, web server and JS mapper. The old default `"2"` means Content Taxonomy 2.0 in AdCOM, so receivers read 3.x IDs as 2.0 codes.
- npm package: ships the full catalogs the Python package already had (3.1: 704 categories; 2.2: 1,197). The published 1.0.0 shipped a 6-row 3.x sample with IDs that are not in the official file (for example "3-5-2" for Cooking, which is 216).
- `synonyms_3x.json`: keyed by label, which is how both mappers look synonyms up. The path-keyed entries never matched, and "Team Sports" is not a 3.1 label.
- README examples use real 3.1 IDs and `cattax: "9"`.
- A record with a 2.x `code` and no `label` now maps: the label is looked up in the bundled 2.x catalog (1,197 codes). Before, `{"code": "1"}` returned no IDs. A supplied label still wins; `label` is optional in the TypeScript `InputRecord`.

## [0.3.1] - 2025-09-10
- CLI: add `update-catalogs` subcommand (fetch exact or latest 2.x/3.x)
- Script: `scripts/update_catalogs.py` robust TSV header detection
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@
<strong>📺 Watch the 5-minute walkthrough</strong>
</p>

Map **IAB Content Taxonomy 2.x** labels/codes to **IAB 3.0** locally with a deterministic → fuzzy → (optional) semantic pipeline.
Outputs are **IAB‑3.0–compatible IDs** for OpenRTB/VAST, with optional **vector attributes** (Channel, Type, Format, Language, Source, Environment) and **SCD** awareness.
> **Targets IAB Content Taxonomy 3.1** and emits `cattax: "9"` by default (AdCOM *List: Category Taxonomies*: 7 = 3.0, 9 = 3.1). Every 3.0 ID still exists in 3.1, so pass `cattax: "7"` if a receiver expects 3.0. Versions before 1.1.0 (npm and PyPI) defaulted to `"2"`, which tells a receiver the IDs are Content Taxonomy 2.0.

Map **IAB Content Taxonomy 2.x** labels/codes to **IAB Content Taxonomy 3.1** locally with a deterministic → fuzzy → (optional) semantic pipeline.
Outputs are **IAB 3.1 IDs** for OpenRTB/VAST, with optional **vector attributes** (Channel, Type, Format, Language, Source, Environment) and **SCD** awareness.

> **Available in both Python and JavaScript/TypeScript**

Expand All @@ -71,17 +73,17 @@ The IAB Mapper helps you migrate from IAB Content Taxonomy 2.x to 3.0 by:

1. **Input:** Your existing 2.x codes/labels (CSV or JSON)
2. **Process:** Deterministic matching → fuzzy matching → optional semantic enhancement
3. **Output:** Valid IAB 3.0 IDs ready for OpenRTB/VAST integration
3. **Output:** Valid IAB 3.1 IDs ready for OpenRTB/VAST integration

**Example:**
```
# Input: 2.x codes
"1-4","Sports"
"2-12","Food & Drink"

# Output: 3.0 IDs
# Output: 3.1 IDs
"483","Sports"
"3-5-2","Food & Drink > Cooking"
"216","Food & Drink > Cooking"
```

Perfect for ad tech teams, content platforms, and anyone migrating to IAB 3.0.
Expand Down Expand Up @@ -147,7 +149,7 @@ result = mapper.map_record({
})

print(result["openrtb"])
# {"content": {"cat": ["3-5-2", "1026"], "cattax": "2"}}
# {"content": {"cat": ["216", "1026"], "cattax": "9"}}
```

### JavaScript
Expand All @@ -164,7 +166,7 @@ const result = mapper.mapRecord({
});

console.log(result.openrtb);
// { content: { cat: ['3-5-2', '1026'], cattax: '2' } }
// { content: { cat: ['216', '1026'], cattax: '9' } }
```

### TypeScript
Expand Down Expand Up @@ -217,7 +219,7 @@ iab-mapper/
Both implementations provide:

- ✅ **Deterministic alias/exact matching** → fuzzy string matching
- ✅ **IAB 3.0 ID emission** (not just labels) with configurable `cattax` for OpenRTB
- ✅ **IAB 3.1 ID emission** (not just labels) with configurable `cattax` for OpenRTB
- ✅ **Multi-category output** per input
- ✅ **Vector attributes** support (Channel, Type, Format, Language, Source, Environment)
- ✅ **SCD (Sensitive Content) flag** visibility and optional exclusion
Expand Down
18 changes: 10 additions & 8 deletions javascript/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# @mixpeek/iab-mapper

> Local IAB Content Taxonomy 2.x → 3.0 mapper for Node.js with vectors, SCD, OpenRTB/VAST exporters
> Local IAB Content Taxonomy 2.x → 3.1 mapper for Node.js with vectors, SCD, OpenRTB/VAST exporters

[![npm version](https://badge.fury.io/js/@mixpeek%2Fiab-mapper.svg)](https://www.npmjs.com/package/@mixpeek/iab-mapper)
[![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD%202--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause)

Map **IAB Content Taxonomy 2.x** labels/codes to **IAB 3.0** locally with deterministic → fuzzy matching. Outputs are **IAB‑3.0–compatible IDs** for OpenRTB/VAST, with optional **vector attributes** (Channel, Type, Format, Language, Source, Environment) and **SCD** awareness.
> **Targets IAB Content Taxonomy 3.1** and emits `cattax: "9"` by default (AdCOM *List: Category Taxonomies*: 7 = 3.0, 9 = 3.1). Every 3.0 ID still exists in 3.1, so pass `cattax: "7"` if a receiver expects 3.0. Versions before 1.1.0 (npm and PyPI) defaulted to `"2"`, which tells a receiver the IDs are Content Taxonomy 2.0.

Map **IAB Content Taxonomy 2.x** labels/codes to **IAB Content Taxonomy 3.1** locally with deterministic → fuzzy matching. Outputs are **IAB 3.1 IDs** for OpenRTB/VAST, with optional **vector attributes** (Channel, Type, Format, Language, Source, Environment) and **SCD** awareness.

This is the **Node.js/TypeScript** version of the [Python iab-mapper](https://github.com/mixpeek/iab-mapper) package.

Expand All @@ -15,7 +17,7 @@ The IAB Mapper helps you migrate from IAB Content Taxonomy 2.x to 3.0 by:

1. **Input:** Your existing 2.x codes/labels
2. **Process:** Deterministic matching → fuzzy matching
3. **Output:** Valid IAB 3.0 IDs ready for OpenRTB/VAST integration
3. **Output:** Valid IAB 3.1 IDs ready for OpenRTB/VAST integration

**Example:**
```javascript
Expand All @@ -28,7 +30,7 @@ const result = mapper.mapRecord({
});

console.log(result.openrtb);
// { content: { cat: ['3-5-2'], cattax: '2' } }
// { content: { cat: ['216'], cattax: '9' } }
```

Perfect for ad tech teams, content platforms, and anyone migrating to IAB 3.0.
Expand Down Expand Up @@ -56,7 +58,7 @@ const mapper = new Mapper({
fuzzyMethod: 'rapidfuzz', // or 'tfidf'
fuzzyCut: 0.92, // similarity threshold
maxTopics: 3, // max topics per result
cattax: '2' // OpenRTB cattax enum
cattax: '9' // OpenRTB cattax: 9 = Content Taxonomy 3.1
});

// Map a single record
Expand Down Expand Up @@ -103,7 +105,7 @@ console.log(result.openrtb);
| `fuzzyCut` | `0.92` | Similarity threshold (0-1). Higher = stricter matching |
| `maxTopics` | `3` | Maximum number of topics per result |
| `dropScd` | `false` | Exclude Sensitive Content (SCD) categories |
| `cattax` | `'2'` | OpenRTB `content.cattax` enum value |
| `cattax` | `'9'` | OpenRTB `content.cattax` value (9 = Content Taxonomy 3.1, 7 = 3.0) |
| `overridesPath` | — | Path to JSON file with manual override mappings |

## 📥 Input Format
Expand Down Expand Up @@ -232,7 +234,7 @@ const result = mapper.mapRecord({ label: 'Sports' });
const bidRequest = {
site: {
content: result.openrtb.content
// { cat: ['483'], cattax: '2' }
// { cat: ['483'], cattax: '9' }
}
};
```
Expand All @@ -252,7 +254,7 @@ const contentCategories = result.vast_contentcat;
The package includes sample taxonomy data. For production use, replace with official IAB data:

- `data/iab_2x.json` — IAB 2.x taxonomy
- `data/iab_3x.json` — IAB 3.0 taxonomy
- `data/iab_3x.json`: IAB Content Taxonomy 3.1 (704 categories)
- `data/synonyms_2x.json` — 2.x synonyms
- `data/synonyms_3x.json` — 3.0 synonyms
- `data/vectors_*.json` — Vector attribute mappings
Expand Down
9 changes: 1 addition & 8 deletions javascript/data/iab_2x.json

Large diffs are not rendered by default.

10 changes: 1 addition & 9 deletions javascript/data/iab_3x.json

Large diffs are not rendered by default.

6 changes: 1 addition & 5 deletions javascript/data/synonyms_2x.json
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
{
"Sports": ["Athletics", "Sport"],
"Food & Drink": ["Food and Drink", "Cuisine"]
}

{"Sports":["Athletics","Games"],"Food & Drink":["Cuisine","Meals","Food"],"Tobacco":["Cigarettes","Smoking"]}
3 changes: 2 additions & 1 deletion javascript/data/synonyms_3x.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"Sports": ["Athletics", "Sport"],
"Cooking": ["Recipes", "Culinary"]
"Cooking": ["Recipes", "Recipe", "Culinary", "Kitchen", "How-to cooking"],
"Alcoholic Beverages": ["Alcohol", "Wine", "Beer", "Spirits"]
}

4 changes: 2 additions & 2 deletions javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion javascript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mixpeek/iab-mapper",
"version": "1.0.0",
"version": "1.1.0",
"description": "Local IAB Content Taxonomy 2.x -> 3.0 mapper with vectors, SCD, OpenRTB/VAST exporters for Node.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
57 changes: 37 additions & 20 deletions javascript/src/__tests__/mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from 'path';

describe('Mapper', () => {
const dataDir = path.join(__dirname, '../../data');

describe('initialization', () => {
it('should initialize with default config', () => {
expect(() => new Mapper({}, dataDir)).not.toThrow();
Expand All @@ -26,7 +26,7 @@ describe('Mapper', () => {

it('should map a known label', () => {
const topics = mapper.mapTopics('Sports');

expect(topics.length).toBeGreaterThan(0);
expect(topics[0]).toHaveProperty('id');
expect(topics[0]).toHaveProperty('label');
Expand All @@ -37,14 +37,14 @@ describe('Mapper', () => {
it('should return empty array for unknown label with high threshold', () => {
const mapper = new Mapper({ fuzzyCut: 0.99 }, dataDir);
const topics = mapper.mapTopics('xyz123nonsense');

expect(topics).toEqual([]);
});

it('should respect maxTopics limit', () => {
const mapper = new Mapper({ maxTopics: 2 }, dataDir);
const topics = mapper.mapTopics('Sports');

expect(topics.length).toBeLessThanOrEqual(2);
});
});
Expand All @@ -54,7 +54,7 @@ describe('Mapper', () => {

it('should map a simple record', () => {
const result = mapper.mapRecord({ label: 'Sports' });

expect(result).toHaveProperty('in_label', 'Sports');
expect(result).toHaveProperty('out_ids');
expect(result).toHaveProperty('topic_ids');
Expand All @@ -65,7 +65,7 @@ describe('Mapper', () => {

it('should include code in output if provided', () => {
const result = mapper.mapRecord({ code: '1-4', label: 'Sports' });

expect(result.in_code).toBe('1-4');
});

Expand All @@ -75,38 +75,56 @@ describe('Mapper', () => {
channel: 'editorial',
type: 'article'
});

expect(result.vectors).toHaveProperty('channel', 'editorial');
expect(result.vectors).toHaveProperty('type', 'article');
expect(result.out_ids.length).toBeGreaterThan(1); // Should include vector IDs
});

it('should map a 2.x code with no label by looking the label up', () => {
const result = mapper.mapRecord({ code: '1' });
expect(result.in_label).toBe('Automotive');
expect(result.out_ids).toContain('1');
expect(result.openrtb.content.cattax).toBe('9');
});

it('should prefer the given label over the code lookup', () => {
const result = mapper.mapRecord({ code: '1', label: 'Cooking' });
expect(result.out_ids).toContain('216');
expect(result.out_ids).not.toContain('1');
});

it('should return no topics for an unknown code with no label', () => {
const result = mapper.mapRecord({ code: 'no-such-code' });
expect(result.topic_ids).toEqual([]);
});

it('should format OpenRTB correctly', () => {
const result = mapper.mapRecord({ label: 'Sports' });

expect(result.openrtb).toHaveProperty('content');
expect(result.openrtb.content).toHaveProperty('cat');
expect(result.openrtb.content).toHaveProperty('cattax', '2');
expect(result.openrtb.content).toHaveProperty('cattax', '9');
expect(Array.isArray(result.openrtb.content.cat)).toBe(true);
});

it('should format VAST ContentCat correctly', () => {
const result = mapper.mapRecord({ label: 'Sports' });

expect(typeof result.vast_contentcat).toBe('string');
expect(result.vast_contentcat).toMatch(/^"[^"]+"(,"[^"]+")*$/); // Format: "id1","id2",...
});

it('should include SCD flags', () => {
const result = mapper.mapRecord({ label: 'Sports' });

expect(Array.isArray(result.topic_scd)).toBe(true);
expect(result.topic_scd.length).toBe(result.topic_ids.length);
});

it('should include confidence scores', () => {
const result = mapper.mapRecord({ label: 'Sports' });

expect(Array.isArray(result.topic_confidence)).toBe(true);
expect(result.topic_confidence.length).toBe(result.topic_ids.length);
result.topic_confidence.forEach(conf => {
Expand All @@ -118,7 +136,7 @@ describe('Mapper', () => {
it('should use custom cattax value', () => {
const mapper = new Mapper({ cattax: '3' }, dataDir);
const result = mapper.mapRecord({ label: 'Sports' });

expect(result.cattax).toBe('3');
expect(result.openrtb.content.cattax).toBe('3');
});
Expand All @@ -132,9 +150,9 @@ describe('Mapper', () => {
{ label: 'Sports' },
{ label: 'Food & Drink' }
];

const results = mapper.mapRecords(records);

expect(results.length).toBe(2);
expect(results[0].in_label).toBe('Sports');
expect(results[1].in_label).toBe('Food & Drink');
Expand All @@ -150,14 +168,14 @@ describe('Mapper', () => {
it('should work with rapidfuzz method', () => {
const mapper = new Mapper({ fuzzyMethod: 'rapidfuzz' }, dataDir);
const result = mapper.mapRecord({ label: 'Sports' });

expect(result.out_ids.length).toBeGreaterThan(0);
});

it('should work with tfidf method', () => {
const mapper = new Mapper({ fuzzyMethod: 'tfidf' }, dataDir);
const result = mapper.mapRecord({ label: 'Sports' });

expect(result.out_ids.length).toBeGreaterThan(0);
});
});
Expand All @@ -167,7 +185,7 @@ describe('Mapper', () => {
// This test would need SCD data to properly verify
const mapper = new Mapper({ dropScd: true }, dataDir);
const result = mapper.mapRecord({ label: 'Sports' });

// All SCD flags should be false if dropScd is true
result.topic_scd.forEach(scd => {
expect(scd).toBe(false);
Expand All @@ -191,9 +209,8 @@ describe('Mapper', () => {
it('should not duplicate IDs in output', () => {
const result = mapper.mapRecord({ label: 'Sports' });
const uniqueIds = new Set(result.out_ids);

expect(uniqueIds.size).toBe(result.out_ids.length);
});
});
});

4 changes: 2 additions & 2 deletions javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* fuzzyMethod: 'rapidfuzz',
* fuzzyCut: 0.92,
* maxTopics: 3,
* cattax: '2'
* cattax: '9'
* });
*
* const result = mapper.mapRecord({
Expand All @@ -22,7 +22,7 @@
* });
*
* console.log(result.openrtb);
* // { content: { cat: ['3-5-2', ...], cattax: '2' } }
* // { content: { cat: ['3-5-2', ...], cattax: '9' } }
* ```
*/

Expand Down
Loading
Loading