← back to Norma
agents/onboard-agent/lib/ntee-matcher.js
84 lines
/**
* NTEE Code to Issue Area Mapper
*
* Maps National Taxonomy of Exempt Entities (NTEE) codes
* to human-readable issue areas relevant to nonprofit advocacy,
* education, consumer protection, and civic engagement.
*/
// NTEE Major Groups relevant to nonprofit advocacy / education / consumer protection
const NTEE_ISSUE_MAP = {
'B': ['education', 'higher_education'],
'B40': ['higher_education'],
'B41': ['college_universities'],
'B42': ['graduate_schools'],
'B43': ['technical_schools'],
'B60': ['student_services'],
'B82': ['scholarships'],
'B83': ['student_financial_aid'],
'I': ['crime_legal', 'consumer_protection'],
'I80': ['legal_services'],
'P': ['human_services'],
'R': ['civil_rights'],
'S': ['community_development'],
'W': ['public_policy', 'advocacy'],
'W20': ['government_reform'],
'W30': ['military_veteran'],
};
/**
* Get issue areas for a given NTEE code.
* Checks exact match, major group (first char), and 3-char prefix.
*
* @param {string} nteeCode - NTEE code (e.g. 'B83', 'W20', 'I')
* @returns {string[]} Array of issue area strings
*/
function getIssueAreas(nteeCode) {
if (!nteeCode) return [];
const issues = new Set();
const major = nteeCode.charAt(0);
// Major group match (single letter)
if (NTEE_ISSUE_MAP[major]) {
NTEE_ISSUE_MAP[major].forEach(i => issues.add(i));
}
// Exact match
if (NTEE_ISSUE_MAP[nteeCode]) {
NTEE_ISSUE_MAP[nteeCode].forEach(i => issues.add(i));
}
// 3-char prefix match (e.g. 'B83' from 'B830')
const prefix = nteeCode.substring(0, 3);
if (prefix !== nteeCode && prefix !== major && NTEE_ISSUE_MAP[prefix]) {
NTEE_ISSUE_MAP[prefix].forEach(i => issues.add(i));
}
return [...issues];
}
/**
* Compute overlap score between org issue areas and topic keywords.
*
* @param {string[]} orgIssues - Issue areas derived from NTEE code
* @param {string[]} topicKeywords - Keywords from the pulse topic
* @returns {number} Score from 0 to 1
*/
function issueOverlapScore(orgIssues, topicKeywords) {
if (!orgIssues.length || !topicKeywords.length) return 0;
const topicLower = topicKeywords.map(k => k.toLowerCase());
let matches = 0;
for (const issue of orgIssues) {
if (topicLower.some(k => issue.includes(k) || k.includes(issue))) {
matches++;
}
}
return Math.min(1, matches / Math.max(orgIssues.length, topicLower.length));
}
module.exports = { NTEE_ISSUE_MAP, getIssueAreas, issueOverlapScore };