use crate::entity::EntityClassifier;
use crate::features::{extract_features, FEATURE_DIM};
use crate::inference::{ClassificationResult, InferenceError, ValueClassifier};
use crate::model2vec_shared::Model2VecResources;
use crate::multi_branch::MultiBranchClassifier;
use crate::rhh;
use finetype_core::{Designation, Taxonomy};
use rayon::prelude::*;
use std::collections::HashMap;
struct NoopClassifier;
impl ValueClassifier for NoopClassifier {
fn classify(&self, _text: &str) -> Result<ClassificationResult, InferenceError> {
Err(InferenceError::InvalidPath(
"NoopClassifier should never be called — multi-branch bypasses ValueClassifier".into(),
))
}
fn classify_batch(
&self,
_texts: &[String],
) -> Result<Vec<ClassificationResult>, InferenceError> {
Err(InferenceError::InvalidPath(
"NoopClassifier should never be called — multi-branch bypasses ValueClassifier".into(),
))
}
}
#[derive(Debug, Clone)]
pub struct ColumnFeatures {
pub mean: [f32; FEATURE_DIM],
pub variance: [f32; FEATURE_DIM],
pub min: [f32; FEATURE_DIM],
pub max: [f32; FEATURE_DIM],
}
impl ColumnFeatures {
fn empty() -> Self {
Self {
mean: [0.0f32; FEATURE_DIM],
variance: [0.0f32; FEATURE_DIM],
min: [0.0f32; FEATURE_DIM],
max: [0.0f32; FEATURE_DIM],
}
}
}
pub fn aggregate_features(per_value: &[[f32; FEATURE_DIM]]) -> ColumnFeatures {
if per_value.is_empty() {
return ColumnFeatures::empty();
}
let n = per_value.len() as f32;
let mut mean = [0.0f32; FEATURE_DIM];
let mut min_vals = per_value[0];
let mut max_vals = per_value[0];
for features in per_value {
for i in 0..FEATURE_DIM {
mean[i] += features[i];
if features[i] < min_vals[i] {
min_vals[i] = features[i];
}
if features[i] > max_vals[i] {
max_vals[i] = features[i];
}
}
}
for m in &mut mean {
*m /= n;
}
let mut variance = [0.0f32; FEATURE_DIM];
for features in per_value {
for i in 0..FEATURE_DIM {
let diff = features[i] - mean[i];
variance[i] += diff * diff;
}
}
for v in &mut variance {
*v /= n;
}
ColumnFeatures {
mean,
variance,
min: min_vals,
max: max_vals,
}
}
mod feature_idx {
pub const IS_FLOAT: usize = 2;
pub const HAS_LEADING_ZERO: usize = 3;
#[allow(dead_code)]
pub const IS_HEX_STRING: usize = 7;
pub const LENGTH: usize = 10;
pub const DIGIT_RATIO: usize = 17;
pub const SEGMENT_COUNT_DOT: usize = 24;
pub const SEGMENT_COUNT_SLASH: usize = 26;
#[allow(dead_code)]
pub const HAS_COLON: usize = 32;
#[allow(dead_code)]
pub const HAS_DASH: usize = 33;
pub const ALPHA_RATIO: usize = 18;
pub const HAS_NEGATIVE_PREFIX: usize = 34;
#[allow(dead_code)]
pub const HAS_PERCENT: usize = 35;
}
fn strip_locale_suffix(label: &str) -> (&str, Option<&str>) {
if let Some((prefix, suffix)) = label.rsplit_once('.') {
let is_locale = !suffix.is_empty()
&& suffix.len() <= 10
&& suffix.chars().all(|c| c.is_ascii_uppercase() || c == '_')
&& prefix.contains('.'); if is_locale {
(prefix, Some(suffix))
} else {
(label, None)
}
} else {
(label, None)
}
}
const BOOLEAN_LABELS: &[&str] = &[
"representation.boolean.binary", "representation.boolean.initials", "representation.boolean.terms", "technology.development.boolean", "representation.logical.boolean", "technology.data.boolean", ];
const LOCATION_TYPES: &[&str] = &[
"geography.location.city",
"geography.location.country",
"geography.location.region",
"geography.location.state",
"geography.location.continent",
];
const PERSON_NAME_HINTS: &[&str] = &[
"identity.person.full_name",
"identity.person.last_name",
"identity.person.first_name",
];
const USERNAME_WHITESPACE_MAX_FRACTION: f32 = 0.15;
const USERNAME_MIN_DISTINCT_FRACTION: f32 = 0.9;
fn is_username_handle_shaped(values: &[String]) -> bool {
let non_empty = non_empty_trimmed(values);
if non_empty.len() < 4 {
return false;
}
let mut with_space = 0usize;
let mut handle_charset = 0usize;
for v in &non_empty {
if v.chars().any(char::is_whitespace) {
with_space += 1;
} else if v.chars().any(|c| c.is_ascii_alphabetic())
&& v.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
{
handle_charset += 1;
}
}
let n = non_empty.len() as f32;
let space_frac = with_space as f32 / n;
let handle_frac = handle_charset as f32 / n;
let distinct = non_empty
.iter()
.collect::<std::collections::HashSet<_>>()
.len();
let distinct_frac = distinct as f32 / n;
space_frac <= USERNAME_WHITESPACE_MAX_FRACTION
&& handle_frac >= 0.80
&& distinct_frac >= USERNAME_MIN_DISTINCT_FRACTION
}
const HARDCODED_GENERIC_LABELS: &[&str] = &[
"representation.text.word",
"representation.text.plain_text",
"representation.numeric.integer_number",
"representation.numeric.decimal_number",
"representation.identifier.increment",
"datetime.component.day_of_month",
"identity.person.username",
"identity.person.first_name",
"identity.person.phone_number",
"geography.transportation.iata_code",
];
fn is_generic_prediction(
label: &str,
disambiguation_rule: &Option<String>,
taxonomy: Option<&Taxonomy>,
) -> bool {
if disambiguation_rule
.as_ref()
.is_some_and(|r| r.starts_with("attractor_demotion"))
{
return true;
}
if disambiguation_rule
.as_ref()
.is_some_and(|r| r == "numeric_postal_code_detection")
{
return true;
}
if BOOLEAN_LABELS.contains(&label) {
return true;
}
if HARDCODED_GENERIC_LABELS.contains(&label) {
return true;
}
if let Some(taxonomy) = taxonomy {
if let Some(def) = taxonomy.get(label) {
return matches!(
def.designation,
Designation::BroadWords
| Designation::BroadCharacters
| Designation::BroadNumbers
| Designation::BroadObject
);
}
}
false
}
const CEDED_RECOVERY_LEAVES: &[&str] = &[
"container.key_value.query_string",
"container.object.html",
"container.object.json",
"container.object.json_array",
"container.object.xml",
"datetime.component.day_of_week",
"datetime.component.month_name",
"datetime.component.periodicity",
"finance.banking.iban",
"finance.crypto.bitcoin_address",
"finance.crypto.ethereum_address",
"finance.currency.currency_code",
"finance.currency.currency_symbol",
"finance.rate.basis_points",
"finance.securities.figi",
"geography.address.street_suffix",
"geography.coordinate.dms",
"geography.coordinate.mgrs",
"geography.coordinate.plus_code",
"geography.format.wkt",
"geography.location.continent",
"geography.transportation.iso6346",
"identity.commerce.isrc",
"identity.government.pan_india",
"identity.government.vin",
"identity.medical.icd10",
"identity.person.email",
"identity.person.email_display",
"identity.person.gender",
"identity.person.phone_e164",
"representation.boolean.terms",
"representation.file.mime_type",
"representation.format.color_hsl",
"representation.identifier.uuid",
"representation.numeric.si_number",
"representation.scientific.inchi",
"representation.text.emoji",
"technology.cloud.aws_arn",
"technology.cloud.s3_uri",
"technology.code.doi",
"technology.cryptographic.jwt",
"technology.identifier.ulid",
"technology.internet.cidr",
"technology.internet.data_uri",
"technology.internet.http_method",
"technology.internet.ip_v4",
"technology.internet.ip_v4_with_port",
"technology.internet.ip_v6",
"technology.internet.mac_address",
"technology.internet.urn",
"technology.internet.user_agent",
];
fn label_validates_sample(tax: &Taxonomy, leaf: &str, sample: &[String]) -> bool {
let Some(validator) = finetype_core::validator::resolve_label_validator(leaf, tax) else {
return false;
};
let mut checked = 0usize;
let mut passed = 0usize;
let mut remaining = sample.len();
for v in sample {
remaining -= 1;
let t = v.trim();
if t.is_empty() {
continue;
}
checked += 1;
if validator.is_valid(t) {
passed += 1;
}
if ((passed + remaining) as f64) / ((checked + remaining) as f64) < 0.9 {
return false;
}
}
checked > 0 && (passed as f64) / (checked as f64) >= 0.9
}
fn sample_contradicts_label(tax: &Taxonomy, leaf: &str, sample: &[String]) -> bool {
let Some(validator) = tax.get_validator(leaf) else {
return false;
};
let non_empty = non_empty_trimmed(sample);
if non_empty.len() < 3 {
return false;
}
let pass = non_empty.iter().filter(|v| validator.is_valid(v)).count();
(pass as f64) / (non_empty.len() as f64) <= 0.10
}
fn values_form_increment(values: &[String]) -> Option<bool> {
let n_nonempty = values.iter().filter(|v| !v.trim().is_empty()).count();
let ints: Vec<i64> = values
.iter()
.filter_map(|v| v.trim().parse::<i64>().ok())
.collect();
if ints.len() < 5 {
return None;
}
let all_int = ints.len() as f32 >= 0.95 * n_nonempty.max(1) as f32;
let distinct = ints
.iter()
.copied()
.collect::<std::collections::HashSet<_>>()
.len();
let min = *ints.iter().min().unwrap();
let max = *ints.iter().max().unwrap();
let span = (max - min + 1) as f64; let contiguous = span > 0.0 && distinct as f64 / span >= 0.80; let near_unique = distinct as f64 / ints.len() as f64 >= 0.90; Some(all_int && min >= 0 && distinct >= 5 && contiguous && near_unique)
}
fn detect_locale_from_validation(
values: &[String],
label: &str,
taxonomy: &Taxonomy,
) -> Option<String> {
let locale_validators = taxonomy.get_locale_validators(label)?;
let non_empty: Vec<&str> = values
.iter()
.map(|s| s.as_str())
.filter(|s| !s.is_empty())
.collect();
if non_empty.is_empty() {
return None;
}
let mut best_locale: Option<String> = None;
let mut best_pass_rate: f32 = 0.0;
for (locale, validator) in locale_validators {
let pass_count = non_empty
.iter()
.filter(|v| validator.validate(v).is_valid)
.count();
let pass_rate = pass_count as f32 / non_empty.len() as f32;
if pass_rate > best_pass_rate {
best_pass_rate = pass_rate;
best_locale = Some(locale.clone());
}
}
if best_pass_rate > 0.5 {
best_locale
} else {
None
}
}
#[derive(Debug, Clone)]
pub struct ColumnConfig {
pub sample_size: usize,
pub min_agreement: f32,
}
impl Default for ColumnConfig {
fn default() -> Self {
Self {
sample_size: 100,
min_agreement: 0.3,
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnResult {
pub label: String,
pub confidence: f32,
pub vote_distribution: Vec<(String, f32)>,
pub disambiguation_applied: bool,
pub disambiguation_rule: Option<String>,
pub samples_used: usize,
pub detected_locale: Option<String>,
pub is_generic: bool,
pub column_features: Option<ColumnFeatures>,
}
pub struct ColumnClassifier {
classifier: Box<dyn ValueClassifier>,
config: ColumnConfig,
taxonomy: Option<Taxonomy>,
entity_classifier: Option<EntityClassifier>,
model2vec: Option<Model2VecResources>,
multi_branch: Option<MultiBranchClassifier>,
skip_sharpen: bool,
}
impl ColumnClassifier {
pub fn new(classifier: Box<dyn ValueClassifier>, config: ColumnConfig) -> Self {
Self {
classifier,
config,
taxonomy: None,
skip_sharpen: false,
entity_classifier: None,
model2vec: None,
multi_branch: None,
}
}
pub fn with_defaults(classifier: Box<dyn ValueClassifier>) -> Self {
Self::new(classifier, ColumnConfig::default())
}
pub fn set_taxonomy(&mut self, taxonomy: Taxonomy) {
self.taxonomy = Some(taxonomy);
}
pub fn set_entity_classifier(&mut self, entity: EntityClassifier) {
self.entity_classifier = Some(entity);
}
pub fn set_model2vec(&mut self, model2vec: Model2VecResources) {
self.model2vec = Some(model2vec);
}
pub fn with_multi_branch(multi_branch: MultiBranchClassifier, config: ColumnConfig) -> Self {
let dummy = Box::new(NoopClassifier);
Self {
classifier: dummy,
config,
taxonomy: None,
entity_classifier: None,
model2vec: None,
multi_branch: Some(multi_branch),
skip_sharpen: false,
}
}
pub fn set_skip_sharpen(&mut self, skip: bool) {
self.skip_sharpen = skip;
}
pub fn has_multi_branch(&self) -> bool {
self.multi_branch.is_some()
}
pub fn classify_column(&self, values: &[String]) -> Result<ColumnResult, InferenceError> {
if let Some(ref mb) = self.multi_branch {
return self.classify_multi_branch(mb, values, "");
}
if values.is_empty() {
return Ok(ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
detected_locale: None,
is_generic: false,
column_features: None,
});
}
let sample = if values.len() <= self.config.sample_size {
values.to_vec()
} else {
let step = values.len() as f64 / self.config.sample_size as f64;
(0..self.config.sample_size)
.map(|i| values[(i as f64 * step) as usize].clone())
.collect()
};
let n_samples = sample.len();
let results = self.classifier.classify_batch(&sample)?;
let mut vote_counts_3level: HashMap<String, usize> = HashMap::new();
let mut locale_votes: HashMap<String, HashMap<String, usize>> = HashMap::new(); for result in &results {
let (base_label, locale) = strip_locale_suffix(&result.label);
*vote_counts_3level
.entry(base_label.to_string())
.or_default() += 1;
if let Some(loc) = locale {
*locale_votes
.entry(base_label.to_string())
.or_default()
.entry(loc.to_string())
.or_default() += 1;
}
}
let mut votes: Vec<(String, usize)> = vote_counts_3level.into_iter().collect();
votes.sort_by_key(|b| std::cmp::Reverse(b.1));
if let Some(taxonomy) = self.taxonomy.as_ref() {
let non_empty_count = sample.iter().filter(|v| !v.trim().is_empty()).count();
if non_empty_count >= 3 {
let validated: Vec<(String, usize)> = votes
.iter()
.filter(|(label, _)| {
taxonomy
.get_validator(label)
.map(|validator| {
let pass_count = sample
.iter()
.filter(|v| validator.is_valid(v.trim()))
.count();
pass_count as f32 / non_empty_count as f32 >= 0.5
})
.unwrap_or(true) })
.cloned()
.collect();
if !validated.is_empty() {
votes = validated;
}
}
}
let vote_distribution: Vec<(String, f32)> = votes
.iter()
.map(|(label, count)| (label.clone(), *count as f32 / n_samples as f32))
.collect();
let (majority_label, majority_count) = votes.first().cloned().unwrap_or_default();
let majority_fraction = majority_count as f32 / n_samples as f32;
let detected_locale = locale_votes.get(&majority_label).and_then(|locales| {
locales
.iter()
.max_by_key(|(_, count)| *count)
.map(|(locale, _)| locale.clone())
.filter(|l| l != "UNIVERSAL") });
let disambiguation =
disambiguate(&sample, &results, &votes, n_samples, self.taxonomy.as_ref());
let mut result = if let Some((label, rule_name)) = disambiguation {
let disambig_locale = locale_votes.get(&label).and_then(|locales| {
locales
.iter()
.max_by_key(|(_, count)| *count)
.map(|(locale, _)| locale.clone())
.filter(|l| l != "UNIVERSAL")
});
let confidence = if rule_name.starts_with("attractor_demotion") {
majority_fraction.max(0.5)
} else {
majority_fraction.max(0.8) };
ColumnResult {
label,
confidence,
vote_distribution,
disambiguation_applied: true,
disambiguation_rule: Some(rule_name),
samples_used: n_samples,
detected_locale: disambig_locale,
is_generic: false,
column_features: None,
}
} else {
let confidence = if majority_fraction >= self.config.min_agreement {
majority_fraction
} else {
majority_fraction * 0.5 };
ColumnResult {
label: majority_label,
confidence,
vote_distribution,
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: n_samples,
detected_locale,
is_generic: false,
column_features: None,
}
};
self.apply_username_veto(&mut result, &sample);
if result.label == "identity.person.full_name" {
if let Some(ref entity_model) = self.entity_classifier {
match entity_model.should_demote(&sample) {
Ok(true) => {
result.label = "representation.text.entity_name".to_string();
result.disambiguation_applied = true;
result.disambiguation_rule = Some("entity_demotion:nonperson".to_string());
result.detected_locale = None;
}
Ok(false) => {} Err(e) => {
tracing::warn!("Entity classifier error, skipping demotion: {}", e);
}
}
}
}
let per_value_features: Vec<[f32; FEATURE_DIM]> =
sample.iter().map(|v| extract_features(v)).collect();
let column_features = aggregate_features(&per_value_features);
result.column_features = Some(column_features.clone());
feature_disambiguate(&mut result, &column_features, &votes, n_samples);
if let Some(taxonomy) = self.taxonomy.as_ref() {
schema_validation_gate(&mut result, &sample, &votes, taxonomy);
}
if let Some(taxonomy) = self.taxonomy.as_ref() {
if let Some(locale) = detect_locale_from_validation(&sample, &result.label, taxonomy) {
result.detected_locale = Some(locale);
}
}
self.finalize_is_generic(&mut result);
Ok(result)
}
fn apply_username_veto(&self, result: &mut ColumnResult, sample: &[String]) {
if result.label == "identity.person.full_name"
&& !rhh::is_disabled("full_name_username_veto")
&& is_username_handle_shaped(sample)
{
result.label = "identity.person.username".to_string();
result.disambiguation_applied = true;
result.disambiguation_rule = Some("full_name_username_veto".to_string());
result.detected_locale = None;
}
}
fn finalize_is_generic(&self, result: &mut ColumnResult) {
result.is_generic = is_generic_prediction(
&result.label,
&result.disambiguation_rule,
self.taxonomy.as_ref(),
);
}
pub fn classifier(&self) -> &dyn ValueClassifier {
&*self.classifier
}
pub fn classify_columns_with_context(
&self,
columns: &[(Vec<String>, String)], ) -> Result<Vec<ColumnResult>, InferenceError> {
columns
.par_iter()
.map(|(values, header)| self.classify_column_with_header(values, header))
.collect()
}
pub fn classify_column_with_header(
&self,
values: &[String],
header: &str,
) -> Result<ColumnResult, InferenceError> {
if let Some(ref mb) = self.multi_branch {
return self.classify_multi_branch(mb, values, header);
}
let mut result = self.classify_column(values)?;
if result
.disambiguation_rule
.as_ref()
.is_some_and(|r| r.starts_with("entity_demotion"))
{
self.finalize_is_generic(&mut result);
return Ok(result);
}
if !result.label.starts_with("datetime.") {
if let Some(epoch_label) = detect_epoch_seconds(values) {
result.label = epoch_label;
result.confidence = 0.85;
result.disambiguation_applied = true;
result.disambiguation_rule = Some("epoch_seconds_range_detection".to_string());
self.finalize_is_generic(&mut result);
return Ok(result);
}
}
let hinted_type: Option<String> = header_hint(header).map(|h| h.to_string());
let hint_is_hardcoded_legacy = hinted_type.is_some();
if let Some(hinted_type) = hinted_type.as_deref() {
if result.label == hinted_type {
result.confidence = (result.confidence + 0.1).min(1.0);
self.finalize_is_generic(&mut result);
return Ok(result);
}
const MEASUREMENT_TYPES: &[&str] =
&["identity.person.height", "identity.person.weight"];
const COORDINATE_TYPES: &[&str] = &[
"geography.coordinate.latitude",
"geography.coordinate.longitude",
];
if MEASUREMENT_TYPES.contains(&hinted_type)
&& MEASUREMENT_TYPES.contains(&result.label.as_str())
{
result.label = hinted_type.to_string();
result.confidence = 0.9;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_measurement:{}", header.to_lowercase()));
self.finalize_is_generic(&mut result);
return Ok(result);
}
if hinted_type == "representation.numeric.decimal_number"
&& COORDINATE_TYPES.contains(&result.label.as_str())
{
result.label = hinted_type.to_string();
result.confidence = 0.8;
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_sci_measurement:{}",
header.to_lowercase()
));
self.finalize_is_generic(&mut result);
return Ok(result);
}
let hint_in_votes = result
.vote_distribution
.iter()
.any(|(label, _)| label == hinted_type);
let is_generic = is_generic_prediction(
&result.label,
&result.disambiguation_rule,
self.taxonomy.as_ref(),
);
if PERSON_NAME_HINTS.contains(&hinted_type) {
if LOCATION_TYPES.contains(&result.label.as_str()) {
result.confidence = result.confidence.max(0.5);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_location_keep:{}",
header.to_lowercase()
));
self.finalize_is_generic(&mut result);
return Ok(result);
}
if is_generic {
let top_location = result
.vote_distribution
.iter()
.filter(|(label, _)| LOCATION_TYPES.contains(&label.as_str()))
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
if let Some((loc_label, loc_frac)) = top_location {
if *loc_frac >= 0.10 {
result.label = loc_label.clone();
result.confidence = loc_frac.max(0.5);
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_location:{}", header.to_lowercase()));
self.finalize_is_generic(&mut result);
return Ok(result);
}
}
}
}
if LOCATION_TYPES.contains(&hinted_type)
&& LOCATION_TYPES.contains(&result.label.as_str())
&& result.label != hinted_type
&& result.confidence <= 0.90
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_geo_override:{}",
header.to_lowercase()
));
self.finalize_is_generic(&mut result);
return Ok(result);
}
if hint_is_hardcoded_legacy && result.label != hinted_type && result.confidence <= 0.80
{
let hint_category = hinted_type.rsplitn(2, '.').last().unwrap_or("");
let pred_category = result.label.rsplitn(2, '.').last().unwrap_or("");
if !hint_category.is_empty()
&& hint_category == pred_category
&& hint_category.contains('.')
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.7);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_same_category:{}",
header.to_lowercase()
));
self.finalize_is_generic(&mut result);
return Ok(result);
}
}
if hint_is_hardcoded_legacy && result.label != hinted_type {
let hint_domain = hinted_type.split('.').next().unwrap_or("");
let pred_domain = result.label.split('.').next().unwrap_or("");
let hint_base = hinted_type.rsplit('.').next().unwrap_or("");
let pred_base = result.label.rsplit('.').next().unwrap_or("");
if !hint_domain.is_empty()
&& !pred_domain.is_empty()
&& hint_domain != pred_domain
&& hint_base != pred_base
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.5);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_cross_domain:{}",
header.to_lowercase()
));
self.finalize_is_generic(&mut result);
return Ok(result);
}
}
let original_label = result.label.clone();
if (result.confidence < 0.5 || is_generic) && hint_in_votes {
let hint_fraction = result
.vote_distribution
.iter()
.find(|(label, _)| label == hinted_type)
.map(|(_, frac)| *frac)
.unwrap_or(0.0);
result.label = hinted_type.to_string();
result.confidence = hint_fraction.max(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!("header_hint:{}", header.to_lowercase()));
} else if hint_is_hardcoded_legacy && result.confidence < 0.5 && !hint_in_votes {
result.label = hinted_type.to_string();
result.confidence = 0.6;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_hardcoded:{}", header.to_lowercase()));
} else if is_generic && !hint_in_votes {
let is_financial_hint = hinted_type.starts_with("finance.");
if is_financial_hint && !hint_is_hardcoded_legacy {
} else {
result.label = hinted_type.to_string();
result.confidence = 0.5;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_generic:{}", header.to_lowercase()));
}
} else if result.confidence < 0.3 && !hint_in_votes {
result.label = hinted_type.to_string();
result.confidence = 0.4;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_fallback:{}", header.to_lowercase()));
}
if result.label != original_label {
result.detected_locale = self
.taxonomy
.as_ref()
.and_then(|t| detect_locale_from_validation(values, &result.label, t));
}
}
self.finalize_is_generic(&mut result);
Ok(result)
}
pub fn config(&self) -> &ColumnConfig {
&self.config
}
fn run_sharpen(
&self,
result: &mut ColumnResult,
header: &str,
sample: &[String],
values: &[String],
column_features: &ColumnFeatures,
) {
if !self.skip_sharpen {
feature_sharpen(result, column_features);
if let Some((resolved_label, rule_name)) = value_sharpen(
sample,
&result.label,
result.confidence,
self.taxonomy.as_ref(),
) {
result.label = resolved_label;
result.disambiguation_applied = true;
result.disambiguation_rule = Some(rule_name);
}
self.datetime_format_refinement(result, sample);
self.structured_string_refinement(result, sample);
self.sharpen_and_guard(result, header, sample, values);
self.apply_username_veto(result, sample);
}
if let Some(taxonomy) = self.taxonomy.as_ref() {
if let Some(locale) = detect_locale_from_validation(sample, &result.label, taxonomy) {
result.detected_locale = Some(locale);
}
}
self.finalize_is_generic(result);
}
fn classify_multi_branch(
&self,
mb: &MultiBranchClassifier,
values: &[String],
header: &str,
) -> Result<ColumnResult, InferenceError> {
if values.is_empty() {
return Ok(ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
detected_locale: None,
is_generic: false,
column_features: None,
});
}
let sample = if values.len() <= self.config.sample_size {
values.to_vec()
} else {
let step = values.len() as f64 / self.config.sample_size as f64;
(0..self.config.sample_size)
.map(|i| values[(i as f64 * step) as usize].clone())
.collect()
};
let samples_used = sample.len();
let (label, confidence) = mb.classify_column(&sample, header, self.taxonomy.as_ref())?;
let per_value_features: Vec<[f32; FEATURE_DIM]> =
sample.iter().map(|v| extract_features(v)).collect();
let column_features = aggregate_features(&per_value_features);
let mut result = ColumnResult {
label: label.clone(),
confidence,
vote_distribution: vec![(label, confidence)],
disambiguation_applied: false,
disambiguation_rule: Some("multi-branch".to_string()),
samples_used,
detected_locale: None,
is_generic: false,
column_features: Some(column_features.clone()),
};
if !self.skip_sharpen {
if let Ok(inj) = std::env::var("FINETYPE_INJECT_LABEL") {
if !inj.is_empty() {
result.label = inj;
result.confidence = 1.0;
}
}
}
self.run_sharpen(&mut result, header, &sample, values, &column_features);
Ok(result)
}
pub fn compose_from_sense(
&self,
header: &str,
values: &[String],
sense_label: &str,
sense_conf: f32,
) -> Result<ColumnResult, InferenceError> {
if values.is_empty() {
return Ok(ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
detected_locale: None,
is_generic: false,
column_features: None,
});
}
let sample = if values.len() <= self.config.sample_size {
values.to_vec()
} else {
let step = values.len() as f64 / self.config.sample_size as f64;
(0..self.config.sample_size)
.map(|i| values[(i as f64 * step) as usize].clone())
.collect()
};
let samples_used = sample.len();
let per_value_features: Vec<[f32; FEATURE_DIM]> =
sample.iter().map(|v| extract_features(v)).collect();
let column_features = aggregate_features(&per_value_features);
let mut result = ColumnResult {
label: sense_label.to_string(),
confidence: sense_conf,
vote_distribution: vec![(sense_label.to_string(), sense_conf)],
disambiguation_applied: false,
disambiguation_rule: Some("compose-from-sense".to_string()),
samples_used,
detected_locale: None,
is_generic: false,
column_features: Some(column_features.clone()),
};
self.run_sharpen(&mut result, header, &sample, values, &column_features);
Ok(result)
}
fn apply_header_sharpen(&self, result: &mut ColumnResult, header: &str, sample: &[String]) {
let label_before = result.label.clone();
if !rhh::is_disabled("header_hint_coord_veto")
&& matches!(
result.label.as_str(),
"geography.coordinate.latitude" | "geography.coordinate.longitude"
)
&& !header_corroborates_coordinate(header)
&& values_look_like_generic_decimals(sample)
{
result.label = "representation.numeric.decimal_number".to_string();
result.confidence = result.confidence.min(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("coord_header_veto:{}", header.to_lowercase()));
return;
}
if !rhh::is_disabled("header_hint_postal_veto")
&& result.label == "geography.address.postal_code"
&& !header_corroborates_postal(header)
&& values_look_like_generic_integers(sample)
{
result.label = "representation.numeric.integer_number".to_string();
result.confidence = result.confidence.min(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("postal_header_veto:{}", header.to_lowercase()));
return;
}
if !rhh::is_disabled("header_hint_state_code_promote")
&& result.label != "geography.location.state_code"
&& header_corroborates_state(header)
&& values_look_like_state_codes(sample)
{
result.label = "geography.location.state_code".to_string();
result.confidence = result.confidence.max(0.8);
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("state_code_promote:{}", header.to_lowercase()));
return;
}
let disable_measurement = rhh::is_disabled("header_hint_measurement");
let disable_sci_measurement = rhh::is_disabled("header_hint_sci_measurement");
let disable_person_override = rhh::is_disabled("header_hint_person_override");
let disable_geo_override = rhh::is_disabled("header_hint_geo_override");
let disable_same_category = rhh::is_disabled("header_hint_same_category");
let disable_cross_domain = rhh::is_disabled("header_hint_cross_domain");
let disable_catchall = rhh::is_disabled("header_hint");
let disable_generic = rhh::is_disabled("header_hint_generic");
let disable_hardcoded = rhh::is_disabled("header_hint_hardcoded");
let hinted_type: Option<String> = header_hint(header).map(|h| h.to_string());
let hint_is_hardcoded = hinted_type.is_some();
let hinted_type = match hinted_type.as_deref() {
Some(h) => h,
None => return,
};
if result.label == hinted_type {
result.confidence = (result.confidence + 0.1).min(1.0);
return;
}
if result
.disambiguation_rule
.as_deref()
.is_some_and(|rule| SHAPE_RECOVERY_RULES.contains(&rule))
&& hinted_type.rsplit_once('.').map(|(c, _)| c)
!= result.label.rsplit_once('.').map(|(c, _)| c)
{
return;
}
const CORROBORATION_SCOPE: &[&str] = &[
"datetime.component.year",
"datetime.epoch.unix_seconds",
"datetime.epoch.unix_milliseconds",
];
if !rhh::is_disabled("header_hint_value_corroboration") {
let contradicted = if hinted_type == "technology.internet.url" {
let (is_bare, _) = values_look_like_bare_numbers(sample);
!is_bare && values_are_clearly_non_url(sample)
} else if CORROBORATION_SCOPE.contains(&hinted_type) {
self.taxonomy
.as_ref()
.is_some_and(|tax| sample_contradicts_label(tax, hinted_type, sample))
} else {
false
};
if contradicted {
return;
}
}
if result.disambiguation_rule.as_deref() == Some("gender_detection")
&& matches!(
hinted_type,
"identity.person.gender" | "identity.person.gender_code"
)
{
return;
}
const MEASUREMENT_TYPES: &[&str] = &["identity.person.height", "identity.person.weight"];
const COORDINATE_TYPES: &[&str] = &[
"geography.coordinate.latitude",
"geography.coordinate.longitude",
];
if !disable_measurement
&& MEASUREMENT_TYPES.contains(&hinted_type)
&& MEASUREMENT_TYPES.contains(&result.label.as_str())
{
result.label = hinted_type.to_string();
result.confidence = 0.9;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_measurement:{}", header.to_lowercase()));
return;
}
if !disable_sci_measurement
&& hinted_type == "representation.numeric.decimal_number"
&& COORDINATE_TYPES.contains(&result.label.as_str())
{
result.label = hinted_type.to_string();
result.confidence = 0.8;
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_sci_measurement:{}",
header.to_lowercase()
));
return;
}
let hint_in_votes = result
.vote_distribution
.iter()
.any(|(label, _)| label == hinted_type);
let is_generic = is_generic_prediction(
&result.label,
&result.disambiguation_rule,
self.taxonomy.as_ref(),
);
if PERSON_NAME_HINTS.contains(&hinted_type)
&& LOCATION_TYPES.contains(&result.label.as_str())
{
if !disable_person_override && hint_is_hardcoded {
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_person_override:{}",
header.to_lowercase()
));
}
return;
}
if !disable_geo_override
&& LOCATION_TYPES.contains(&hinted_type)
&& LOCATION_TYPES.contains(&result.label.as_str())
&& result.label != hinted_type
&& (hint_is_hardcoded || result.confidence <= 0.90)
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_geo_override:{}",
header.to_lowercase()
));
return;
}
if hinted_type == "datetime.timestamp.iso_8601"
&& hint_is_hardcoded
&& result.label.starts_with("datetime.")
&& result.label != "datetime.timestamp.iso_8601"
{
return;
}
if !disable_same_category && hint_is_hardcoded && result.label != hinted_type {
let hint_category = hinted_type.rsplitn(2, '.').last().unwrap_or("");
let pred_category = result.label.rsplitn(2, '.').last().unwrap_or("");
if !hint_category.is_empty()
&& hint_category == pred_category
&& hint_category.contains('.')
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.7);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_same_category:{}",
header.to_lowercase()
));
}
}
if !disable_cross_domain && hint_is_hardcoded && result.label != hinted_type {
let hint_domain = hinted_type.split('.').next().unwrap_or("");
let pred_domain = result.label.split('.').next().unwrap_or("");
let hint_base = hinted_type.rsplit('.').next().unwrap_or("");
let pred_base = result.label.rsplit('.').next().unwrap_or("");
if !hint_domain.is_empty()
&& !pred_domain.is_empty()
&& hint_domain != pred_domain
&& hint_base != pred_base
{
result.label = hinted_type.to_string();
result.confidence = result.confidence.max(0.5);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"header_hint_cross_domain:{}",
header.to_lowercase()
));
return;
}
}
if !disable_catchall && (result.confidence < 0.5 || is_generic) && hint_in_votes {
let hint_fraction = result
.vote_distribution
.iter()
.find(|(label, _)| label == hinted_type)
.map(|(_, frac)| *frac)
.unwrap_or(0.0);
result.label = hinted_type.to_string();
result.confidence = hint_fraction.max(0.6);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!("header_hint:{}", header.to_lowercase()));
} else if !disable_generic && is_generic && !hint_in_votes {
let is_financial_hint = hinted_type.starts_with("finance.");
if is_financial_hint && !hint_is_hardcoded {
tracing::debug!(
column = %header,
blocked_hint = %hinted_type,
current_label = %result.label,
"Financial model2vec hint blocked (no vote evidence)"
);
} else {
result.label = hinted_type.to_string();
result.confidence = 0.5;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_generic:{}", header.to_lowercase()));
}
} else if !disable_hardcoded && hint_is_hardcoded && !hint_in_votes {
let h_domain = hinted_type.split('.').next().unwrap_or("");
let p_domain = result.label.split('.').next().unwrap_or("");
let threshold = if h_domain != p_domain { 0.85 } else { 0.95 };
if result.confidence < threshold {
result.label = hinted_type.to_string();
result.confidence = 0.5;
result.disambiguation_applied = true;
result.disambiguation_rule =
Some(format!("header_hint_hardcoded:{}", header.to_lowercase()));
}
}
if result.label == "geography.location.country" {
let non_empty = non_empty_trimmed(sample);
if non_empty.len() >= 3 {
let alpha2_count = non_empty
.iter()
.filter(|v| v.len() == 2 && v.chars().all(|c| c.is_ascii_uppercase()))
.count();
let alpha2_rate = alpha2_count as f32 / non_empty.len() as f32;
if alpha2_rate >= 0.95 {
result.label = "geography.location.country_code".to_string();
result.confidence = result.confidence.max(0.8);
result.disambiguation_applied = true;
result.disambiguation_rule = Some(format!(
"country_code_post_hint_guard:alpha2_rate={:.2}",
alpha2_rate
));
}
}
}
if result.label != label_before {
if let Some(taxonomy) = self.taxonomy.as_ref() {
result.detected_locale =
detect_locale_from_validation(sample, &result.label, taxonomy);
}
}
}
fn sharpen_and_guard(
&self,
result: &mut ColumnResult,
header: &str,
sample: &[String],
values: &[String],
) {
if header.is_empty() {
return;
}
self.apply_header_sharpen(result, header, sample);
self.apply_post_sharpen_guards(result, header, sample, values);
}
fn datetime_format_refinement(&self, result: &mut ColumnResult, sample: &[String]) {
if rhh::is_disabled("datetime_format_refinement") {
return;
}
let detected = match finetype_core::datetime_format::detect_datetime_format(sample) {
Some(d) => d,
None => return,
};
if !detected.delimited && !result.label.starts_with("datetime.") {
return;
}
if result.label == detected.leaf {
return;
}
match self.taxonomy.as_ref() {
Some(tax) if label_validates_sample(tax, detected.leaf, sample) => {}
_ => return,
}
result.label = detected.leaf.to_string();
if detected.delimited {
result.confidence = result.confidence.max(0.99);
}
result.disambiguation_applied = true;
result.disambiguation_rule = Some("datetime_format_refinement".to_string());
result.detected_locale = None;
}
fn structured_string_refinement(&self, result: &mut ColumnResult, sample: &[String]) {
if rhh::is_disabled("structured_string_refinement") {
return;
}
let tax = match self.taxonomy.as_ref() {
Some(t) => t,
None => return,
};
const RESIDUAL: &[&str] = &[
"representation.text.plain_text",
"representation.text.word",
"unknown",
];
const URL_FIRE_ON: &[&str] = &[
"representation.text.plain_text",
"representation.text.word",
"unknown",
"technology.internet.ip_v6",
"container.object.xml",
"technology.cloud.aws_arn",
"finance.crypto.bitcoin_address",
"technology.cryptographic.jwt",
"representation.text.entity_name",
"geography.address.full_address",
"representation.identifier.alphanumeric_id",
"identity.government.eu_vat",
];
let url_fire_on: &[&str] = if rhh::is_disabled("url_override_tier") {
RESIDUAL
} else {
URL_FIRE_ON
};
let readers: [(&str, &[&str]); 4] = [
(
"technology.filesystem.windows_path",
&[
"representation.text.plain_text",
"representation.text.word",
"unknown",
"technology.internet.url",
"technology.internet.urn",
],
),
(
"technology.internet.message_id",
&[
"representation.text.plain_text",
"representation.text.word",
"unknown",
"identity.person.email",
],
),
("technology.internet.url", url_fire_on),
("technology.code.qualified_name", RESIDUAL),
];
for (leaf, fire_on) in readers {
if fire_on.contains(&result.label.as_str()) && label_validates_sample(tax, leaf, sample)
{
result.label = leaf.to_string();
result.confidence = result.confidence.max(0.99);
result.disambiguation_applied = true;
result.disambiguation_rule = Some("structured_string_refinement".to_string());
result.detected_locale = None;
return;
}
}
}
}
mod feature_sharpen;
mod guards;
mod header_sharpen;
mod helpers;
mod substance;
mod value_sharpen;
pub(crate) use feature_sharpen::*;
pub(crate) use header_sharpen::*;
pub(crate) use helpers::*;
pub(crate) use substance::*;
pub(crate) use value_sharpen::*;
#[cfg(test)]
mod tests;