Skip to main content

Cultural Relevance System

Last Updated: November 17, 2025
Status: Production (Phase 6)
Scope: Estonian-first cultural intelligence for gift recommendations


Table of Contents

  1. Overview
  2. Architecture
  3. Cultural Rules Service
  4. Age-Based Cultural Filtering
  5. Estonian Cultural Calendar
  6. Estonian Product Prioritization
  7. Gender Appropriateness
  8. Integration Points
  9. Testing & Validation
  10. Performance Metrics
  11. Future Enhancements

Overview

The Cultural Relevance System provides multi-dimensional cultural intelligence specifically optimized for the Estonian market. Unlike generic internationalization approaches that simply translate text, this system deeply understands Estonian culture, customs, and social norms.

Key Capabilities

  • Age-appropriate filtering - Prevents culturally inappropriate gifts (e.g., alcohol to children, toys to elderly)
  • Estonian occasion awareness - Recognizes 9 Estonian-specific holidays with budget inference
  • Local product prioritization - Boosts Estonian authors, publishers, and cultural products
  • Gender sensitivity - Adjusts scores for gender-appropriate gifts
  • Language-aware processing - Estonian morphology handling and bilingual support

Design Philosophy

Cultural Appropriateness > Raw Relevance Scores

The system enforces cultural rules as hard constraints in the funnel stage (Stage B) before expensive LLM reranking, ensuring:

  • No culturally inappropriate products reach the user
  • Local cultural products are naturally prioritized
  • Estonian customs are respected throughout the pipeline

Architecture

System Flow Overview

Key Stages:

  1. Language Detection - Determines if user is Estonian, English, or mixed
  2. Context Extraction - Extracts age, recipient, occasion from query
  3. Cultural Filtering - Applies age-appropriate hard constraints
  4. Estonian Boosting - Prioritizes local products for Estonian users
  5. Final Selection - Culturally relevant products ranked and displayed

Cultural Rules Service

Location: app/api/chat/services/language.ts

Core Service Class

export class LanguageService {
// Language detection
static detectLanguage(message: string): 'et' | 'en' | 'mixed'

// Cultural rules
static getCulturalRules(): CulturalRules
static getEstonianOccasions(): EstonianOccasion[]
static getBudgetHintByOccasion(occasion: string): BudgetRange | null

// Estonian morphology
static toEstonianDative(noun: string): string
static normalizeRecipient(recipient: string): string
static normalizeOccasion(occasion: string): string

// Product filtering & boosting
static boostEstonianProducts(products: Product[], language: string): Product[]
static filterByAge(products: Product[], ageGroup: string): Product[]
}

Cultural Rules Data Structure

export interface CulturalRules {
elderlyForbidden: string[]; // Products inappropriate for 60+ recipients
childForbidden: string[]; // Products inappropriate for 0-12 recipients
teenForbidden: string[]; // Products inappropriate for 13-19 recipients
}

const CULTURAL_RULES: CulturalRules = {
elderlyForbidden: [
'mänguasjad', 'beebi', 'laste', 'nukkude', 'teismeliste',
'gaming', 'videomängud', 'baby', 'toys', 'kids',
'mähkmed', 'diapers'
],

childForbidden: [
'alkohol', 'vein', 'spirits', 'alcohol', 'wine', 'beer', 'õlu'
],

teenForbidden: [
'beebi', 'baby', 'väikelaste', 'toddler', 'mähkmed'
]
};

Age-Based Cultural Filtering

Location: app/api/chat/services/stage-b.ts:395-507

How It Works

Age-based filtering ensures products are culturally appropriate for the recipient's age group.

Implementation Strategy

Age-based filtering operates on three levels:

1. Hard Cultural Rules (Forbidden Categories)

Applied to elderly, child, and teen recipients:

function passesAgeBasedFilters(
candidate: FunnelCandidate,
env: StageBEnvironment,
state: StageBState
): boolean {
const { age, culturalRules, debug } = env;

// ELDERLY FILTER (60+)
if (age.isElderlyRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
const productType = (candidate.product_type || '').toLowerCase();
const title = (candidate.title || '').toLowerCase();

if (
culturalRules.elderlyForbidden.some(
forbidden =>
category.includes(forbidden) ||
productType.includes(forbidden) ||
title.includes(forbidden)
)
) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}

// CHILD FILTER (0-12)
if (age.isChildRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
if (culturalRules.childForbidden.some(forbidden =>
category.includes(forbidden)
)) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}

// TEEN FILTER (13-19)
if (age.isTeenRecipient) {
const category = (candidate.csv_category || '').toLowerCase();
const title = (candidate.title || '').toLowerCase();
if (
culturalRules.teenForbidden.some(
forbidden => category.includes(forbidden) || title.includes(forbidden)
)
) {
state.categoryFiltered++;
return false; // HARD REJECT
}
}

return true; // PASSES AGE FILTER
}

Implementation note: child alcohol filtering currently inspects only csv_category (not title/product_type). Alcoholic items with neutral categories can slip through.

2. Infant Product Detection

Comprehensive keyword matching for infant-specific products:

const infantKeywords = [
// Estonian terms
'beebi', 'imiku', 'imikut', 'väikelapse', 'väikelast',
'vastsündinu', 'mähkme', 'mähkmed', 'pisipõnn', 'pisipõnnidele',

// English terms
'baby', 'infant', 'toddler', 'newborn',

// Product-specific
'lutipudel', 'kõristi', 'beebitekk', 'beebilapp', 'kaisulapp',

// Age ranges
'vanus 0-3', 'vanus 0-2', 'vanus 0-5',

// Brand-specific (Estonian characters)
'lotte tegelane', // Lotte character (ages 3-6)
'lotte multifilm', // Lotte cartoon
'roosi', // Roosi character (baby sister)
'väike õde' // Little sister (toddler-focused)
];

Logic:

if (age.shouldAvoidInfantProducts && !age.isExplicitBabyGift) {
const haystack = [
candidate.csv_category,
candidate.product_type,
candidate.title
].join(' ').toLowerCase();

if (infantKeywords.some(keyword => haystack.includes(keyword))) {
return false; // REJECT: Infant product for non-infant recipient
}
}

3. Content Difficulty Filter

Location: stage-b.ts:509-567

Prevents advanced/professional content for children:

function passesContentDifficultyFilter(
candidate: FunnelCandidate,
env: StageBEnvironment,
state: StageBState
): boolean {
const age = env.context.recipientAge;

// Only apply to children (age < 13)
if (!age || age >= 13) {
return true;
}

const text = [
candidate.title || '',
candidate.csv_category || '',
candidate.description || ''
].join(' ').toLowerCase();

// Detect advanced/professional content
const ADVANCED_INDICATORS = [
'professional', 'advanced', 'theory', 'technical',
'collector', 'gallery', 'exhibition', 'catalog',
'näitus', 'professionaal', 'teooria', 'tehniline',
'eksklusiivne', 'kollektsionäär'
];

const hasAdvancedContent = ADVANCED_INDICATORS.some(kw => text.includes(kw));

if (hasAdvancedContent) {
return false; // REJECT: Too advanced for child
}

return true;
}

Filter Execution Order

Cultural filters (F2 and F3) ensure age-appropriate products before expensive operations.


Estonian Cultural Calendar

Location: language.ts:77-132

Estonian-Specific Occasions

The system understands 9 major Estonian holidays and automatically infers appropriate gift budgets.

Budget Inference - When these occasions are detected, the system automatically suggests appropriate price ranges:

Estonian-Specific Occasions

The system recognizes 9 major Estonian occasions with culturally-appropriate budget ranges:

const ESTONIAN_OCCASIONS: EstonianOccasion[] = [
{
name: 'Jaanipäev', // MAJOR Estonian holiday
date: '06-23', // Midsummer Day
typicalBudget: { min: 15, max: 40 },
keywords: ['jaanipäev', 'jaani', 'jaanikingitus']
},
{
name: 'Kadripäev', // St. Catherine's Day
date: '11-25',
typicalBudget: { min: 10, max: 25 },
keywords: ['kadripäev', 'kadri', 'kadrikingitus']
},
{
name: 'Mardipäev', // St. Martin's Day
date: '11-10',
typicalBudget: { min: 10, max: 25 },
keywords: ['mardipäev', 'mardi', 'martikingitus']
},
{
name: 'Isadepäev', // Father's Day
date: '11-14', // Different from US (June)
typicalBudget: { min: 20, max: 60 },
keywords: ['isadepäev', 'isa päev', 'isale']
},
{
name: 'Emadepäev', // Mother's Day
date: '05-09', // Fixed date (not floating Sunday)
typicalBudget: { min: 20, max: 60 },
keywords: ['emadepäev', 'ema päev', 'emale']
},
{
name: 'Valentinipäev', // Valentine's Day / Friend's Day
date: '02-14',
typicalBudget: { min: 15, max: 50 },
keywords: ['valentinipäev', 'valentine', 'sõbrapäev']
},
{
name: 'Naistepäev', // Women's Day (ex-Soviet tradition)
date: '03-08', // International Women's Day
typicalBudget: { min: 15, max: 40 },
keywords: ['naistepäev', 'naisele', '8. märts']
},
{
name: 'Jõulud', // Christmas
date: '12-24', // Christmas Eve (main celebration)
typicalBudget: { min: 25, max: 80 },
keywords: ['jõulud', 'jõulu', 'christmas', 'jõulukingitus']
},
{
name: 'Uusaasta', // New Year
date: '12-31',
typicalBudget: { min: 20, max: 60 },
keywords: ['uusaasta', 'uue aasta', 'new year', 'aastavahetuse']
}
];

Budget Inference Logic

Location: language.ts:300-328

static getBudgetHintByOccasion(occasion: string): BudgetRange | null {
if (!occasion) return null;

const normalizedOccasion = occasion.toLowerCase().trim();

// PRIORITY 1: Check Estonian cultural occasions first
for (const estonianOccasion of ESTONIAN_OCCASIONS) {
const matched = estonianOccasion.keywords.some(keyword =>
normalizedOccasion.includes(keyword)
);

if (matched) {
return {
min: estonianOccasion.typicalBudget.min,
max: estonianOccasion.typicalBudget.max,
hint: `${estonianOccasion.name} kingitus`
};
}
}

// PRIORITY 2: Check general occasion budgets
for (const [occasionKey, budget] of Object.entries(OCCASION_BUDGETS)) {
if (normalizedOccasion.includes(occasionKey)) {
return budget;
}
}

return null; // No budget hint available
}

General Occasion Budgets

Location: language.ts:138-150

const OCCASION_BUDGETS: Record<string, BudgetRange> = {
'sünnipäev': { min: 20, max: 50, hint: 'keskmine sünnipäevakingitus' },
'tänu': { min: 10, max: 30, hint: 'tähelepanuavaldus' },
'väike tähelepanu': { min: 5, max: 15, hint: 'väike kingitus' },
'sissekolimine': { min: 30, max: 80, hint: 'sissekolimiskingitus' },
'pulm': { min: 50, max: 150, hint: 'pulmakingitus' },
'pensionile minek': { min: 30, max: 80, hint: 'pensionile jäämise kingitus' },
'lapse sünd': { min: 30, max: 80, hint: 'beebi kingitus' },
'paranemine': { min: 10, max: 30, hint: 'tervenemissoovidega' },
'juubel': { min: 40, max: 100, hint: 'tähtpäevakingitus' },
'lõpetamine': { min: 25, max: 70, hint: 'lõpukingitus' },
'hobi': { min: 15, max: 50, hint: 'hobiga seotud' }
};

Cultural Significance

Why this matters:

  • Estonian Father's Day is November 14 (not June like US)
  • Estonian Mother's Day is May 9 (fixed date, not floating Sunday)
  • Jaanipäev is a MAJOR holiday (equivalent to July 4th in US)
  • Christmas Eve (Dec 24) is the main celebration (not Dec 25)
  • Budget expectations differ by culture (e.g., wedding gifts: 50-150€ vs $100-300 in US)

Estonian Product Prioritization

Location: language.ts:420-481

How Estonian Products Get Boosted

When a user queries in Estonian, the system automatically prioritizes Estonian cultural products through score multipliers.

Example: Estonian novel by Jaan Kross published by Varrak:

  • 1.15 (author) × 1.15 (category) × 1.1 (publisher) = 1.457× boost (+45.7%)

Boosting Strategy

Estonian products receive multiplicative score boosts when the user's language is Estonian:

static boostEstonianProducts(products: Product[], language: string): Product[] {
// Only boost for Estonian language users
if (language !== 'et') return products;

return products.map(product => {
let boost = 1.0;

// 1. Estonian Authors: +15% boost
const authors = product.authors?.toString().toLowerCase() || '';
if (authors.match(/\b(eesti|estonian)\b/i)) {
boost *= 1.15;
}

// 2. Estonian Category: +15% boost
const category = product.csv_category?.toString() || '';
if (category.includes('Eesti')) {
boost *= 1.15;
}

// 3. Estonian Publishers: +10% boost
if (this.isEstonianPublisher(product)) {
boost *= 1.1;
}

// Apply cumulative boost to score
if (boost > 1.0) {
const currentScore = product.score || 1.0;
return { ...product, score: currentScore * boost };
}

return product;
});
}

Case sensitivity: the category boost checks for the substring "Eesti" with a capital E. Lowercase eesti categories are not boosted in the current implementation.

Estonian Publisher Detection

Location: language.ts:462-481

private static isEstonianPublisher(product: Product): boolean {
const publisher = product.publisher?.toString().toLowerCase() || '';
const title = product.title?.toString().toLowerCase() || '';

const ESTONIAN_PUBLISHERS = [
'varrak', // Major Estonian publisher
'tänapäev', // Contemporary literature
'pegasus', // Children's books
'eesti raamat', // Estonian book
'kunst', // Art publisher
'avita', // Literary publisher
'postimees', // Newspaper/books
'kirjastus', // Generic "publisher" (Estonian word)
'eesti keele sihtasutus' // Estonian Language Foundation
];

// Check publisher field
for (const estonianPub of ESTONIAN_PUBLISHERS) {
if (publisher.includes(estonianPub)) {
return true;
}
}

// Check title for publisher mentions
for (const estonianPub of ESTONIAN_PUBLISHERS) {
if (title.includes(estonianPub)) {
return true;
}
}

return false;
}

Cumulative Boost Examples

ProductEstonian AuthorEstonian CategoryEstonian PublisherTotal BoostFinal Multiplier
Estonian novel by Jaan KrossYesYesYes (Varrak)1.15 × 1.15 × 1.11.457× (+45.7%)
Estonian film (Eesti Film)NoYesNo1.0 × 1.15 × 1.01.15× (+15%)
Translation (foreign author)NoNoYes (Tänapäev)1.0 × 1.0 × 1.11.1× (+10%)
Foreign book (no local ties)NoNoNo1.0 × 1.0 × 1.01.0× (no boost)

Why Multiplicative (not Additive)?

// BAD: Additive boosts
boost = 0.0;
if (estonianAuthor) boost += 0.15;
if (estonianCategory) boost += 0.15;
if (estonianPublisher) boost += 0.10;
finalScore = score * (1.0 + boost); // Max: 1.4× (linear)

// GOOD: Multiplicative boosts
boost = 1.0;
if (estonianAuthor) boost *= 1.15;
if (estonianCategory) boost *= 1.15;
if (estonianPublisher) boost *= 1.1;
finalScore = score * boost; // Max: 1.457× (compound)

Advantages:

  • Products with multiple Estonian signals get disproportionately stronger boosts
  • Reflects real-world preference: "Estonian author + Estonian publisher" is MORE valuable than sum of parts
  • Prevents over-boosting products with single weak signal

Gender Appropriateness

Actual implementation (Phase 4.5, post-rerank):

  • Gender is inferred via detectGenderFromRecipient in app/api/chat/services/gender-affinity.ts:255-320, invoked from the context orchestrator (app/api/chat/orchestrators/context-orchestrator/extract-context.ts:909-937).
  • Boosting is applied later in the search orchestrator (app/api/chat/orchestrators/search-orchestrator.ts:474-528) using category affinity scores and calculateGenderBoostMultiplier (0.5×–1.8×). This happens after Stage B and after rerank.
  • The helper LanguageService.adjustScoreForGender in language.ts is currently unused in the pipeline; Stage B does not perform gender filtering or scoring.

Implications:

  • Gender handling is a soft ranking boost, not a funnel/filter rule.
  • Only products with categories present in CATEGORY_GENDER_AFFINITY (gender-affinity.ts) receive adjustments; missing/neutral categories pass through unchanged.
  • If accurate gender-specific behavior is required earlier in the funnel, code changes would be needed; the document now reflects the current behavior.

Integration Points

1. Language Detection Layer

Location: language.ts:238-278

static detectLanguage(message: string): 'et' | 'en' | 'mixed' {
const estonianWords = /\b(soovita|soovitada|otsin|kingitus|raamat|...)\b/i;
const englishWords = /\b(recommend|search|want|gift|book|...)\b/i;

const hasEstonian = estonianWords.test(message);
const hasEnglish = englishWords.test(message);

if (hasEstonian && hasEnglish) return 'mixed';
if (hasEstonian) return 'et';
if (hasEnglish) return 'en';
return 'et'; // Default to Estonian for Estonian bookstore
}

Rules:

  1. Estonian query ('et') → Estonian prompt → Estonian response
  2. English query ('en') → English prompt → English response
  3. Mixed query ('mixed') → Estonian prompt → Estonian response (default to Estonian)

2. Stage B Funnel (Phase 2)

Location: app/api/chat/services/stage-b.ts

Cultural rules are retrieved and applied during funnel filtering:

function createStageBEnvironment(context: GiftContext, ...): StageBEnvironment {
return {
context,
debug,
excludeSet,
budgets: { ... },
product: { ... },
age: { ... },
allowAdultProducts,
isAdultRecipient,
culturalRules: LanguageService.getCulturalRules(), // ← Cultural rules injected here
negativeConstraintPatterns,
normalizedIntent
};
}

function passesStageBFilters(candidate, env, state): boolean {
if (!passesAgeBasedFilters(candidate, env, state)) return false; // ← Uses culturalRules
// ... other filters
}

3. Search Orchestrator (Phase 6)

Location: app/api/chat/orchestrators/search-orchestrator.ts:298-318

Estonian product boosting is applied after funnel and before reranking:

// Phase 6: Estonian Product Prioritization
if (SearchOrchestratorConfig.PHASE6_ENABLED && giftContext.language === 'et') {
const boostStart = Date.now();

searchResult.products = LanguageService.boostEstonianProducts(
searchResult.products as any[],
giftContext.language
) as any[];

if (debug) {
console.log('ESTONIAN BOOST APPLIED:', {
time: (Date.now() - boostStart) + 'ms',
products: searchResult.products.length,
language: giftContext.language
});
}
}

4. System Prompt Generation

Location: app/api/chat/system-prompt.ts:246-292

Language-aware system prompts:

export function generateDynamicSystemPrompt(
language?: 'et' | 'en' | 'mixed',
context?: { recipient?: string; occasion?: string; productType?: string }
): string {
// Rule: Estonian/mixed → Estonian prompt, English → English prompt
let basePrompt = (!language || language === 'mixed' || language === 'et')
? buildEstonianSystemPrompt()
: buildEnglishSystemPrompt();

// Add context reminders for multi-turn coherence
if (context) {
// Language-aware context reminders (Estonian vs English)
}

return basePrompt;
}

Pipeline Flow with Cultural Intelligence


Testing & Validation

Test Suite

Location: test-phase6-cultural-rules.sh

Dedicated test script for cultural rules and Estonian boost:

#!/bin/bash
# PHASE 6: Bilingual & Cultural Rules Test
# Tests: Estonian language support, cultural appropriateness, Estonian product prioritization

test_phase6() {
local test_name="$1"
local query="$2"
local expected_language="$3" # et, en, mixed
local expect_estonian_boost="$4" # true/false
local cultural_context="$5" # Description of cultural expectation

# Make API request with debug=true
response=$(curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{
\"messages\": [{\"role\": \"user\", \"content\": \"$query\"}],
\"conversationId\": \"test-phase6-$(date +%s)\",
\"debug\": true
}")

# Extract language detection
detected_lang=$(echo "$response" | grep -o '"language":"[^"]*"' | cut -d'"' -f4)

# Check for Estonian boost
estonian_boost_applied=$(echo "$response" | grep -q "ESTONIAN BOOST APPLIED" && echo "true" || echo "false")

# Validate results
# ...
}

Test Scenarios

1. Language Detection Tests

test_phase6 "Estonian Query" \
"Soovita raamat lapsele" \
"et" \
"true" \
"Estonian language → Estonian boost"

test_phase6 "English Query" \
"Recommend a book for a child" \
"en" \
"false" \
"English language → No Estonian boost"

test_phase6 "Mixed Query" \
"Soovita book for lapsele" \
"mixed" \
"true" \
"Mixed language → Default to Estonian (boost applied)"

2. Age-Based Cultural Filtering Tests

test_phase6 "Child Recipient (No Alcohol)" \
"Kingitus 10-aastasele poisile" \
"et" \
"true" \
"Child → Alcohol filtered out"

test_phase6 "Elderly Recipient (No Toys)" \
"Kingitus 75-aastasele vanaemale" \
"et" \
"true" \
"Elderly → Gaming/toys filtered out"

test_phase6 "Teen Recipient (No Baby Products)" \
"Kingitus 15-aastasele tüdrukule" \
"et" \
"true" \
"Teen → Baby products filtered out"

3. Estonian Occasion Tests

test_phase6 "Jaanipäev (Estonian Holiday)" \
"Jaanipäeva kingitus kolleegile" \
"et" \
"true" \
"Jaanipäev → Budget 15-40€ inferred"

test_phase6 "Isadepäev (Estonian Father's Day)" \
"Isadepäeva kingitus" \
"et" \
"true" \
"Isadepäev → Budget 20-60€ inferred (November 14)"

4. Estonian Product Boost Tests

test_phase6 "Estonian Author Query" \
"Andrus Kivirähk raamat" \
"et" \
"true" \
"Estonian author → +15% boost"

test_phase6 "Estonian Film Query" \
"Eesti filmid" \
"et" \
"true" \
"Estonian category → +15% boost"

Manual Test Scenarios

Location: test-scenarios/comprehensive-test-scenarios.md

Documented test cases for cultural relevance:

### AGE-CULTURE-001: Elderly Recipient (No Gaming)
**Query:** "Kingitus 70-aastasele vanaisale"
**Expected:** No gaming, toys, or baby products
**Validation:** Check that elderlyForbidden categories are filtered

### AGE-CULTURE-002: Child Recipient (No Alcohol)
**Query:** "Kingitus 8-aastasele lapsele"
**Expected:** No alcohol products
**Validation:** Check that childForbidden categories are filtered

### OCCASION-CULTURE-001: Jaanipäev Budget
**Query:** "Jaanipäeva kingitus"
**Expected:** Budget inferred as 15-40€
**Validation:** Check context.budgetMin === 15, budgetMax === 40

### ESTONIAN-BOOST-001: Estonian Author
**Query:** "Soovita Jaan Kross raamat"
**Expected:** Estonian author products ranked higher
**Validation:** Check for +15% boost in logs

Test Coverage

FeatureTest TypeLocationCoverage
Language DetectionManual curl smoke (logs inspected)test-phase6-cultural-rules.shInformational only
Age FilteringManual curl + scenario docstest-phase6-cultural-rules.sh, test-scenarios/Informational only
Occasion BudgetManualtest-scenarios/comprehensive-test-scenarios.mdInformational only
Estonian BoostManual curl (log check for boost)test-phase6-cultural-rules.shInformational only
Gender Ranking BoostManual scenariostest-scenarios/Partial; no automated checks

Performance Metrics

No repository-backed production metrics or load-test artifacts are available for this system. All latency/uptime figures are TBD until instrumentation or benchmark logs are added.


Future Enhancements

Planned Improvements (Q1 2026)

1. Dynamic Cultural Rules

Current: Hardcoded in language.ts
Proposed: Database-backed with admin UI

// Future: Load cultural rules from database
const culturalRules = await db.query('culturalRules')
.filter(q => q.eq('market', 'estonia'))
.filter(q => q.eq('active', true))
.collect();

Benefits:

  • Market-specific rules without code changes
  • A/B testing different cultural thresholds
  • Rapid response to cultural sensitivities

2. Cultural Fit Scoring

Current: Binary pass/fail filtering
Proposed: Graduated cultural fit scores (0-1)

interface CulturalFitScore {
ageAppropriateness: number; // 0.0 - 1.0
occasionRelevance: number; // 0.0 - 1.0
localCulturalValue: number; // 0.0 - 1.0
genderAppropriate: number; // 0.0 - 1.0
total: number; // weighted average
}

function calculateCulturalFit(product, context): CulturalFitScore {
// Graduated scoring instead of hard filters
}

Benefits:

  • More nuanced rankings (not just "pass" or "fail")
  • Better handling of edge cases
  • Explainable cultural relevance

3. Regional Variations

Current: Estonia-wide rules
Proposed: Support for regional differences

const REGIONAL_OCCASIONS = {
'tartu': [
{ name: 'Tartu Linnapäev', date: '06-29', budget: { min: 15, max: 35 } }
],
'tallinn': [
{ name: 'Tallinna Linnapäev', date: '05-15', budget: { min: 20, max: 50 } }
],
// ... other cities
};

Benefits:

  • Hyper-local relevance
  • Support for city-specific holidays
  • Regional gift budget differences

4. Cultural Explanation in Responses

Current: Cultural filtering happens silently
Proposed: Surface cultural reasoning to users

interface ProductRecommendation {
product: Product;
relevanceScore: number;
culturalFit: {
score: number;
explanation: string; // NEW
badges: string[]; // NEW: ['Estonian Author', 'Age-Appropriate', 'Local Publisher']
};
}

Example Response:

"This book by Andrus Kivirähk is from Estonian publisher Varrak 
and perfect for Jaanipäev (budget: €25). The content is appropriate
for adults and aligns with Estonian literary traditions."

5. Multi-Market Support

Current: Estonian-only cultural intelligence
Proposed: Extensible to other markets

interface MarketCulturalRules {
market: 'estonia' | 'finland' | 'latvia' | 'lithuania';
language: string;
occasions: EstonianOccasion[];
ageRules: CulturalRules;
localPublishers: string[];
budgetCurrency: string;
}

const MARKET_RULES: Record<string, MarketCulturalRules> = {
estonia: { ... },
finland: { ... }, // Future expansion
latvia: { ... } // Future expansion
};

Research Directions

1. LLM-Based Cultural Filtering

Hypothesis: LLMs can handle nuanced cultural edge cases better than rule-based systems

Experiment:

// Current: Rule-based filtering (fast, deterministic)
if (culturalRules.elderlyForbidden.some(kw => title.includes(kw))) {
return false;
}

// Proposed: LLM-based filtering (slower, more nuanced)
const culturalFit = await llm.evaluate(`
Is "${product.title}" culturally appropriate for a 70-year-old Estonian man?
Consider: age appropriateness, Estonian cultural norms, social expectations.
Respond with: APPROPRIATE, QUESTIONABLE, or INAPPROPRIATE.
`);

Challenges:

  • Latency (50-200ms per product)
  • Cost ($0.01 per 1000 products at scale)
  • Reliability (LLM hallucinations)

2. User Feedback Loop

Proposal: Learn from user behavior to refine cultural rules

// Track user interactions
interface UserFeedback {
userId: string;
productId: string;
query: string;
culturalFitScore: number;
userAction: 'clicked' | 'purchased' | 'skipped';
timestamp: Date;
}

// Analyze patterns
const insights = analyzeUserFeedback({
timeRange: 'last_30_days',
segment: 'elderly_recipients'
});

// Adjust cultural rules based on insights
if (insights.elderlyGamingAcceptance > 0.3) {
// Consider removing "gaming" from elderlyForbidden
}

3. Cross-Cultural Gift Intelligence

Scenario: Estonian user buying gift for someone in Finland

interface CrossCulturalContext {
giverMarket: 'estonia';
recipientMarket: 'finland';
occasion: 'birthday';
culturalTranslation: {
budgetAdjustment: 1.2, // Finnish gifts 20% more expensive
occasionMapping: { 'sünnipäev''syntymäpäivä' },
culturalNotes: ['Finns prefer practical gifts', 'Avoid overly personal items']
};
}

Appendix

A. Estonian Morphology

Estonian is a highly inflected language with 14 grammatical cases. The Cultural Relevance System handles dative case (allatiiw) for gift queries:

// Dative case: "to someone" (kellele? / to whom?)
const ESTONIAN_DATIVE_MAP: Record<string, string> = {
'õpetaja': 'õpetajale', // teacher → to teacher
'sõber': 'sõbrale', // friend → to friend
'ema': 'emale', // mother → to mother
'isa': 'isale', // father → to father
'laps': 'lapsele', // child → to child
// ... 20+ mappings
};

// Usage: "kingitus õpetajale" (gift to teacher)

B. Synonym Normalization

English → Estonian canonical form mapping:

// Recipients
{ pattern: /\b(grandmother|granny)\b/i, canonical: 'vanaema' }
{ pattern: /\b(grandfather|granddad)\b/i, canonical: 'vanaisa' }
{ pattern: /\b(mother|mom)\b/i, canonical: 'ema' }

// Occasions
{ pattern: /\b(birthday|b-day)\b/i, canonical: 'sünnipäev' }
{ pattern: /\b(wedding|marriage)\b/i, canonical: 'pulm' }

C. Cultural Filtering Decision Tree

  • Main Documentation: documentation/docs/quality/estonian-cultural.md
  • Challenges Overview: architecture/estonian-language-llm-challenges.md
  • Best Practices: documentation/docs/quality/estonian-best-practices.md
  • Test Scenarios: test-scenarios/comprehensive-test-scenarios.md
  • Implementation: app/api/chat/services/language.ts
  • Funnel Integration: app/api/chat/services/stage-b.ts

E. Key Metrics (November 2025)

MetricValueNotes
Languages Supported2 (Estonian, English)+ mixed detection
Estonian Occasions9Culturally-specific holidays
Cultural Rules3 categoriesElderly, Child, Teen
Forbidden Keywords25+Age-inappropriate terms
Estonian Publishers9Major Estonian publishers
Dative Case Mappings20+Estonian morphology
Test CoverageManual smoke onlyNo automated coverage metrics in repo
Production UptimeN/ANot instrumented in repo

Conclusion

The Cultural Relevance System represents a sophisticated, multi-layered approach to cultural intelligence that goes far beyond simple translation or localization. By combining:

  1. Hard cultural filtering (safety & appropriateness)
  2. Estonian occasion awareness (local holidays & budgets)
  3. Local product prioritization (Estonian authors & publishers)
  4. Gender sensitivity (category-based ranking boost in Phase 4.5)

...the system delivers recommendations that feel native to Estonian culture rather than generic international suggestions.

Key Success Factors

Estonian-first design - Built for Estonian market (not adapted from Western defaults)
Early enforcement - Cultural rules applied in funnel stage (before expensive operations)
Transparent & debuggable - Logging for cultural decisions (when debug is on)
Testing status: Manual smoke checks only; automated coverage not yet reported
Performance status: Throughput/latency metrics not instrumented in repo


Document Version: 1.0
Last Reviewed: November 17, 2025
Next Review: March 1, 2026
Owner: Cultural Intelligence Team
Reviewers: Product, Engineering, Estonian Cultural Advisors