Generalise Story Intelligence entity consolidation
This commit is contained in:
parent
b7f24954e1
commit
ae0552af90
@ -44,6 +44,8 @@ var tests = new (string Name, Action Test)[]
|
||||
("Asset canonical keys merge trivial variants", AssetCanonicalKeysMergeTrivialVariants),
|
||||
("Asset canonical keys merge contextual vehicle variants", AssetCanonicalKeysMergeContextualVehicleVariants),
|
||||
("Asset canonical keys keep unrelated owned cars distinct", AssetCanonicalKeysKeepUnrelatedOwnedCarsDistinct),
|
||||
("Asset candidate keys attach unique unowned variants", AssetCandidateKeysAttachUniqueUnownedVariants),
|
||||
("Asset filtering rejects relationship facts", AssetFilteringRejectsRelationshipFacts),
|
||||
("Asset contextual names resolve POV ownership", AssetContextualNamesResolvePovOwnership),
|
||||
("Asset structural phrases trim neighbouring nouns", AssetStructuralPhrasesTrimNeighbouringNouns),
|
||||
("Asset significance is generic and not fixture-specific", AssetSignificanceIsGenericAndNotFixtureSpecific),
|
||||
@ -1906,8 +1908,8 @@ static void CharacterFilteringPreservesTitledNames()
|
||||
Assert(!IsGenericGroupReference("Reverend Potter"), "Reverend Potter should not be treated as generic.");
|
||||
Assert(IsNamedOrTitledPerson("Reverend Potter"), "Reverend Potter should be recognised as a titled person.");
|
||||
Assert(IsNamedOrTitledPerson("Detective Sergeant Summerhill"), "Detective Sergeant Summerhill should be recognised as a titled person.");
|
||||
Assert(IsNamedOrTitledPerson("Mrs Patterson"), "Mrs Patterson should be recognised as a titled person.");
|
||||
Assert(IsNamedOrTitledPerson("Aunt Elen"), "Aunt Elen should be recognised as a titled person.");
|
||||
Assert(IsNamedOrTitledPerson("Mrs Alder"), "Mrs Alder should be recognised as a titled person.");
|
||||
Assert(IsNamedOrTitledPerson("Aunt Mara"), "Aunt Mara should be recognised as a titled person.");
|
||||
}
|
||||
|
||||
static bool IsGenericGroupReference(string name)
|
||||
@ -1932,15 +1934,15 @@ static void LocationFilteringRejectsMergedPhrases()
|
||||
Assert(IsMergedLocationPhrase("road and bridge"), "Merged road/bridge phrase should be rejected.");
|
||||
Assert(IsMergedLocationPhrase("kitchen and hallway"), "Merged room phrase should be rejected.");
|
||||
Assert(IsMergedLocationPhrase("front and rear garden"), "Merged garden phrase should be rejected.");
|
||||
Assert(!IsMergedLocationPhrase("Rose and Crown"), "Named pub should not be treated as a merged location.");
|
||||
Assert(!IsMergedLocationPhrase("Fox and Lantern"), "Named pub should not be treated as a merged location.");
|
||||
}
|
||||
|
||||
static void LocationFilteringPreservesStoryLocations()
|
||||
{
|
||||
Assert(IsStoryLocation("Ashdown Trust"), "Ashdown Trust should be preserved.");
|
||||
Assert(IsStoryLocation("St Luke's Church"), "St Luke's Church should be preserved.");
|
||||
Assert(IsStoryLocation("Mrs Patterson's House"), "Mrs Patterson's House should be preserved.");
|
||||
Assert(IsStoryLocation("Bristol Road"), "Named roads should be preserved.");
|
||||
Assert(IsStoryLocation("Harbour Trust"), "Harbour Trust should be preserved.");
|
||||
Assert(IsStoryLocation("St Anselm's Church"), "St Anselm's Church should be preserved.");
|
||||
Assert(IsStoryLocation("Mrs Alder's House"), "Mrs Alder's House should be preserved.");
|
||||
Assert(IsStoryLocation("Northbridge Road"), "Named roads should be preserved.");
|
||||
Assert(!IsStoryLocation("Kitchen"), "Standalone generic rooms should not be story locations.");
|
||||
}
|
||||
|
||||
@ -1948,23 +1950,23 @@ static void LocationCanonicalKeysMergeTrivialVariants()
|
||||
{
|
||||
Assert(LocationCanonicalKey("Kitchen") == LocationCanonicalKey("The Kitchen"), "The Kitchen should fold into Kitchen.");
|
||||
Assert(LocationCanonicalKey("Interview room") == LocationCanonicalKey("Interview Room"), "Case-only room variants should merge.");
|
||||
Assert(LocationCanonicalKey("Ashdown Trust (reception)") == LocationCanonicalKey("Ashdown Trust"), "Parenthetical variants should merge.");
|
||||
Assert(LocationCanonicalKey("Harbour Trust (reception)") == LocationCanonicalKey("Harbour Trust"), "Parenthetical variants should merge.");
|
||||
}
|
||||
|
||||
static void LocationContextualNamesUsePovAndStructuralSetting()
|
||||
{
|
||||
var bethScene = new Scene { StructuralSetting = "Beth's bedroom", POVCharacterName = "Beth" };
|
||||
var maggieScene = new Scene { StructuralSetting = "Maggie's bedroom", POVCharacterName = "Maggie" };
|
||||
var noraScene = new Scene { StructuralSetting = "Nora's bedroom", POVCharacterName = "Nora" };
|
||||
var imaniScene = new Scene { StructuralSetting = "Imani's bedroom", POVCharacterName = "Imani" };
|
||||
|
||||
Assert(ResolveContextualLocationName("my bedroom", "bedroom", bethScene) == "Beth's bedroom", "Beth POV plus Beth structural setting should resolve my bedroom to Beth's bedroom.");
|
||||
Assert(ResolveContextualLocationName("bedroom", "bedroom", maggieScene) == "Maggie's bedroom", "Maggie structural setting should resolve generic bedroom to Maggie's bedroom.");
|
||||
Assert(LocationCanonicalKey("Beth's bedroom") != LocationCanonicalKey("Maggie's bedroom"), "Owned bedrooms for different POV characters must remain distinct.");
|
||||
Assert(ResolveContextualLocationName("Narrator's home", null, bethScene) == "Beth's home", "Narrator-owned locations should resolve through canonical POV.");
|
||||
Assert(ResolveContextualLocationName("my bedroom", "bedroom", noraScene) == "Nora's bedroom", "Nora POV plus Nora structural setting should resolve my bedroom to Nora's bedroom.");
|
||||
Assert(ResolveContextualLocationName("bedroom", "bedroom", imaniScene) == "Imani's bedroom", "Imani structural setting should resolve generic bedroom to Imani's bedroom.");
|
||||
Assert(LocationCanonicalKey("Nora's bedroom") != LocationCanonicalKey("Imani's bedroom"), "Owned bedrooms for different POV characters must remain distinct.");
|
||||
Assert(ResolveContextualLocationName("Narrator's home", null, noraScene) == "Nora's home", "Narrator-owned locations should resolve through canonical POV.");
|
||||
}
|
||||
|
||||
static void LocationCanonicalKeysMergePunctuationAndSafePlurals()
|
||||
{
|
||||
Assert(LocationCanonicalKey("Mrs Patterson's house") == LocationCanonicalKey("Mrs. Patterson's house"), "Title punctuation variants should merge.");
|
||||
Assert(LocationCanonicalKey("Mrs Alder's house") == LocationCanonicalKey("Mrs. Alder's house"), "Title punctuation variants should merge.");
|
||||
Assert(LocationCanonicalKey("garage") == LocationCanonicalKey("garages"), "Safe singular/plural location variants should merge.");
|
||||
Assert(!IsStoryLocation("road"), "Incidental generic road should not be a strong story location.");
|
||||
}
|
||||
@ -1975,18 +1977,20 @@ static void LocationCandidateConsolidationHandlesGenericReviewVariants()
|
||||
Assert(LocationCanonicalKey("The Crown Inn") == LocationCanonicalKey("Crown Inn"), "Leading article variants should merge.");
|
||||
Assert(LocationCanonicalKey("inside The Crown Inn") == LocationCanonicalKey("at the Crown Inn"), "Context wrappers should not create separate candidates.");
|
||||
Assert(LocationCanonicalKey("The Crown Inn interior, early evening") == LocationCanonicalKey("Crown Inn"), "Temporal and broad interior suffixes should fold into the named place.");
|
||||
Assert(LocationCanonicalKey("Mrs Patterson's house") == LocationCanonicalKey("Mrs. Patterson's house"), "Mrs/Mrs. punctuation should not split a house candidate.");
|
||||
Assert(LocationCanonicalKey("Mrs Alder's house") == LocationCanonicalKey("Mrs. Alder's house"), "Mrs/Mrs. punctuation should not split a house candidate.");
|
||||
Assert(LocationCanonicalKey("Dr Hale's house") == LocationCanonicalKey("Dr. Hale's house"), "Unrelated title punctuation fixture should also merge.");
|
||||
|
||||
Assert(LocationCanonicalKey("Nora's house") != LocationCanonicalKey("Imani's house"), "Distinct owned houses must not merge.");
|
||||
Assert(LocationCanonicalKey("The Crown Inn") != LocationCanonicalKey("The Crown Inn cellar"), "Meaningful sub-locations should remain separate.");
|
||||
Assert(LocationCanonicalKey("The Meridian Hotel") != LocationCanonicalKey("The Meridian Hotel kitchen"), "Unrelated named-place sub-location fixture should remain separate.");
|
||||
Assert(LocationCanonicalKey("The Meridian Hotel car park and waiting area") == LocationCanonicalKey("Meridian Hotel car park"), "Low-value waiting-area suffixes should fold into the specific parent setting.");
|
||||
Assert(LocationCanonicalKey("inside Meridian Hotel car park") == LocationCanonicalKey("Meridian Hotel car park"), "Context prefixes should not split specific repeatable settings.");
|
||||
|
||||
Assert(IsGenericLocation("the tunnel"), "Tunnel should be recognised as a generic setting label.");
|
||||
Assert(RepeatableGenericSettingKeys().Contains(LocationCanonicalKey("the tunnel")), "Repeated active tunnel settings should be allowed to survive review.");
|
||||
Assert(!IsStoryLocation("a stretch of road"), "One-off incidental road prose should not be treated as a strong named location.");
|
||||
Assert(IsGenericComposite("Nora's house and Imani's house"), "Multi-location owned-place conjunctions should not become one fake location.");
|
||||
Assert(!IsMergedLocationPhrase("Rose and Crown"), "Generic named compounds should not need a production special-case exception.");
|
||||
Assert(!IsMergedLocationPhrase("Fox and Lantern"), "Generic named compounds should not need a production special-case exception.");
|
||||
}
|
||||
|
||||
static void LocationReviewUxSupportsLargeCandidateSets()
|
||||
@ -2066,12 +2070,12 @@ static void AssetFilteringRejectsGenericObjects()
|
||||
|
||||
static void AssetFilteringPreservesStoryAssets()
|
||||
{
|
||||
Assert(IsAssetNameCandidate("TR6"), "TR6 should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("ZX6R"), "ZX6R should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("red notebook"), "Red notebook should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("passport"), "Passport should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("car keys"), "Car keys should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("driving licence"), "Driving licence should be treated as a story asset.");
|
||||
Assert(IsAssetNameCandidate("Memory Tin"), "Structurally significant Memory Tin-style objects should be treated as story assets.");
|
||||
Assert(IsAssetNameCandidate("brass astrolabe"), "Structurally significant arbitrary objects should be treated as story assets.");
|
||||
}
|
||||
|
||||
static void AssetCanonicalKeysMergeTrivialVariants()
|
||||
@ -2083,37 +2087,59 @@ static void AssetCanonicalKeysMergeTrivialVariants()
|
||||
|
||||
static void AssetCanonicalKeysMergeContextualVehicleVariants()
|
||||
{
|
||||
Assert(AssetCanonicalKey("Triumph TR6") == AssetCanonicalKey("TR6"), "Triumph TR6 and TR6 should merge when the model identifies the specific vehicle.");
|
||||
Assert(AssetCanonicalKey("Kawasaki ZX6R") == AssetCanonicalKey("ZX6R"), "Manufacturer/model-code variants should merge without hard-coded manuscript vehicle names.");
|
||||
Assert(AssetCanonicalKey("Mazda MX5") == AssetCanonicalKey("MX5"), "A second arbitrary manufacturer/model-code fixture should merge the same way.");
|
||||
}
|
||||
|
||||
static void AssetCanonicalKeysKeepUnrelatedOwnedCarsDistinct()
|
||||
{
|
||||
Assert(AssetCanonicalKey("Beth's car") != AssetCanonicalKey("Maggie's car"), "Owned cars should remain separate when context identifies different owners.");
|
||||
Assert(AssetCanonicalKey("Nora's car") != AssetCanonicalKey("Imani's car"), "Generic owned vehicles should remain separate for unrelated books too.");
|
||||
}
|
||||
|
||||
static void AssetCandidateKeysAttachUniqueUnownedVariants()
|
||||
{
|
||||
var existing = new[] { AssetCanonicalKey("Nora's pass certificate") };
|
||||
Assert(FindContextualAssetKey(existing, AssetCanonicalKey("pass certificate")) == existing[0], "A unique unowned asset variant should attach to its owned contextual candidate.");
|
||||
|
||||
var ambiguous = new[]
|
||||
{
|
||||
AssetCanonicalKey("Nora's pass certificate"),
|
||||
AssetCanonicalKey("Imani's pass certificate")
|
||||
};
|
||||
Assert(FindContextualAssetKey(ambiguous, AssetCanonicalKey("pass certificate")) is null, "Unowned asset variants must not merge when multiple owners match.");
|
||||
}
|
||||
|
||||
static void AssetFilteringRejectsRelationshipFacts()
|
||||
{
|
||||
Assert(!IsAssetNameCandidate("Nora's sister Mara"), "Possessive relationship facts should not become assets.");
|
||||
Assert(!IsAssetNameCandidate("Nora's mother dead in the bath"), "Event/family relationship phrases should not become assets.");
|
||||
Assert(!IsAssetNameCandidate("Nora's parents in bed but Ellis"), "Relationship scene facts should not become assets.");
|
||||
Assert(!IsAssetNameCandidate("Nora's number before leaving"), "Information fragments should not become assets.");
|
||||
Assert(IsAssetNameCandidate("Nora's photograph"), "Actual owned asset nouns should survive relationship filtering.");
|
||||
Assert(IsAssetNameCandidate("Nora's address book"), "Actual owned object phrases should survive relationship filtering.");
|
||||
}
|
||||
|
||||
static void AssetContextualNamesResolvePovOwnership()
|
||||
{
|
||||
var scene = new Scene { POVCharacterName = "Beth" };
|
||||
Assert(ResolveContextualAssetName("my memory tin", null, scene) == "Beth's memory tin", "POV ownership should resolve my memory tin to Beth's memory tin.");
|
||||
Assert(ResolveContextualAssetName("Memory Tin", "Beth", scene) == "Beth's Memory Tin", "Explicit owner evidence should prefix owned significant assets.");
|
||||
Assert(IsAssetNameCandidate("Beth's Memory Tin"), "Owned Memory Tin-style significant objects should be retained.");
|
||||
var scene = new Scene { POVCharacterName = "Nora" };
|
||||
Assert(ResolveContextualAssetName("my brass astrolabe", null, scene) == "Nora's brass astrolabe", "POV ownership should resolve my brass astrolabe to Nora's brass astrolabe.");
|
||||
Assert(ResolveContextualAssetName("brass astrolabe", "Nora", scene) == "Nora's brass astrolabe", "Explicit owner evidence should prefix owned significant assets.");
|
||||
Assert(IsAssetNameCandidate("Nora's brass astrolabe"), "Owned arbitrary significant objects should be retained.");
|
||||
|
||||
var unrelatedScene = new Scene { POVCharacterName = "Nora" };
|
||||
Assert(ResolveContextualAssetName("my brass astrolabe", null, unrelatedScene) == "Nora's brass astrolabe", "POV ownership should resolve arbitrary significant owned objects.");
|
||||
var unrelatedScene = new Scene { POVCharacterName = "Imani" };
|
||||
Assert(ResolveContextualAssetName("my brass astrolabe", null, unrelatedScene) == "Imani's brass astrolabe", "POV ownership should resolve arbitrary significant owned objects.");
|
||||
}
|
||||
|
||||
static void AssetStructuralPhrasesTrimNeighbouringNouns()
|
||||
{
|
||||
Assert(CleanStructuralAssetPhrase("memory tin and photographs") == "memory tin", "Structural asset extraction should not bundle neighbouring nouns after and.");
|
||||
Assert(CleanStructuralAssetPhrase("brass astrolabe and photographs") == "brass astrolabe", "Structural asset extraction should not bundle neighbouring nouns after and.");
|
||||
Assert(CleanStructuralAssetPhrase("brass astrolabe containing coordinates") == "brass astrolabe", "Structural asset extraction should trim explanatory tails for arbitrary objects.");
|
||||
}
|
||||
|
||||
static void AssetSignificanceIsGenericAndNotFixtureSpecific()
|
||||
{
|
||||
Assert(IsDistinctiveAssetName("memory tin"), "Memory Tin should survive because it is a distinctive structural object phrase.");
|
||||
Assert(IsDistinctiveAssetName("brass astrolabe"), "A second arbitrary significant object should use the same distinctive-name mechanism.");
|
||||
Assert(IsDistinctiveAssetName("brass astrolabe"), "Arbitrary significant object phrases should survive because they are distinctive.");
|
||||
Assert(IsDistinctiveAssetName("obsidian compass"), "A second arbitrary significant object should use the same distinctive-name mechanism.");
|
||||
Assert(!IsDistinctiveAssetName("cup"), "Single incidental props should not become significant merely by being named.");
|
||||
|
||||
var service = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceAssetImportService.cs"));
|
||||
@ -2250,6 +2276,15 @@ static string AssetCanonicalKey(string name)
|
||||
return (string)method!.Invoke(null, [name])!;
|
||||
}
|
||||
|
||||
static string? FindContextualAssetKey(IEnumerable<string> existingKeys, string candidateKey)
|
||||
{
|
||||
var method = typeof(StoryIntelligenceAssetImportService).GetMethod(
|
||||
"FindContextualAssetKey",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
|
||||
Assert(method is not null, "FindContextualAssetKey was not found.");
|
||||
return (string?)method!.Invoke(null, [existingKeys, candidateKey]);
|
||||
}
|
||||
|
||||
static bool InvokePrivateAssetFilter(string methodName, string name)
|
||||
{
|
||||
var method = typeof(StoryIntelligenceAssetImportService).GetMethod(
|
||||
|
||||
@ -37,7 +37,7 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
"passport", "photograph", "photo", "key", "keys", "car keys", "letter", "envelope", "note",
|
||||
"notebook", "knife", "gun", "suitcase", "rucksack", "wedding ring",
|
||||
"ring", "necklace", "cassette tape", "camera", "map", "diary", "watch", "driving licence",
|
||||
"driving license", "licence", "license", "document", "folder", "wallet", "bank book", "scrap of paper",
|
||||
"driving license", "licence", "license", "document", "folder", "wallet", "bank book", "address book", "scrap of paper",
|
||||
"keepsake box", "keepsake tin");
|
||||
|
||||
private static readonly HashSet<string> StorySignals = CreateSet(
|
||||
@ -48,6 +48,11 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
private static readonly HashSet<string> AdjectivesToFold = CreateSet(
|
||||
"red", "old", "new", "small", "large", "little", "anonymous", "folded", "sealed", "missing");
|
||||
|
||||
private static readonly HashSet<string> RelationshipWords = CreateSet(
|
||||
"mother", "father", "mum", "mom", "dad", "sister", "brother", "parent", "parents", "daughter", "son",
|
||||
"child", "children", "aunt", "uncle", "wife", "husband", "partner", "friend", "neighbour", "neighbor",
|
||||
"teacher", "student", "examiner", "doctor", "nurse");
|
||||
|
||||
public async Task<StoryIntelligenceAssetReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
@ -686,7 +691,9 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
}
|
||||
|
||||
var normalised = Normalise(clean);
|
||||
return !GenericObjects.Contains(normalised) && !GenericObjects.Contains(RemoveLeadingArticle(normalised));
|
||||
return !GenericObjects.Contains(normalised)
|
||||
&& !GenericObjects.Contains(RemoveLeadingArticle(normalised))
|
||||
&& !LooksLikePersonOrRelationshipFact(clean);
|
||||
}
|
||||
|
||||
private static bool IsAcceptedStoryAssetName(string? name)
|
||||
@ -768,7 +775,9 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
|
||||
private static string? FindContextualAssetKey(IEnumerable<string> existingKeys, string candidateKey)
|
||||
{
|
||||
if (candidateKey.Contains(' '))
|
||||
if (string.IsNullOrWhiteSpace(candidateKey)
|
||||
|| HasPossessiveOwner(candidateKey)
|
||||
|| LooksLikePersonOrRelationshipFact(candidateKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -830,6 +839,10 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
|| IsAcceptedStoryAssetName(name)
|
||||
|| IsDistinctiveAssetName(name);
|
||||
|
||||
private static bool HasPossessiveOwner(string value)
|
||||
=> value.Contains("'s ", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Contains("’s ", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string? BuildFirstAppearance(Scene scene, SceneIntelligenceScene parsed, string assetName)
|
||||
{
|
||||
var summary = Clean(FirstConfigured(scene.StructuralSummary, scene.Summary, parsed.Summary?.Short));
|
||||
@ -921,7 +934,7 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
private static bool IsDistinctiveAssetName(string? value)
|
||||
{
|
||||
var clean = RemoveLeadingArticle(Normalise(CleanAssetName(value)));
|
||||
if (string.IsNullOrWhiteSpace(clean) || GenericObjects.Contains(clean))
|
||||
if (string.IsNullOrWhiteSpace(clean) || GenericObjects.Contains(clean) || LooksLikePersonOrRelationshipFact(clean))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -942,6 +955,39 @@ public sealed class StoryIntelligenceAssetImportService(
|
||||
&& words.Any(word => !AdjectivesToFold.Contains(word) && !GenericObjects.Contains(word));
|
||||
}
|
||||
|
||||
private static bool LooksLikePersonOrRelationshipFact(string? value)
|
||||
{
|
||||
var clean = RemoveLeadingArticle(Normalise(CleanAssetName(value)));
|
||||
if (string.IsNullOrWhiteSpace(clean) || IsAcceptedStoryAssetName(clean))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clean.Contains(" dead in ", StringComparison.OrdinalIgnoreCase)
|
||||
|| clean.Contains(" in bed", StringComparison.OrdinalIgnoreCase)
|
||||
|| clean.Contains(" before leaving", StringComparison.OrdinalIgnoreCase)
|
||||
|| clean.Contains(" written name", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var subject = StripPossessiveOwner(clean);
|
||||
var words = subject.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
return words.Length is >= 1 and <= 4
|
||||
&& words.Any(word => RelationshipWords.Contains(word))
|
||||
&& !words.Any(word => AcceptedAssetNames.Contains(word) || StorySignals.Contains(word));
|
||||
}
|
||||
|
||||
private static string StripPossessiveOwner(string value)
|
||||
{
|
||||
var straightIndex = value.IndexOf("'s ", StringComparison.OrdinalIgnoreCase);
|
||||
var curlyIndex = value.IndexOf("’s ", StringComparison.OrdinalIgnoreCase);
|
||||
var index = straightIndex >= 0
|
||||
? straightIndex
|
||||
: curlyIndex;
|
||||
return index >= 0 ? value[(index + 3)..] : value;
|
||||
}
|
||||
|
||||
private static string? ExtractDistinctiveModelCode(string? value)
|
||||
{
|
||||
var clean = Normalise(value);
|
||||
|
||||
@ -413,7 +413,7 @@ public sealed class StoryIntelligenceLocationImportService(
|
||||
return;
|
||||
}
|
||||
|
||||
var key = identity.Key;
|
||||
var key = ResolveCandidateKey(groups, identity.Key, identity.DisplayName);
|
||||
if (!groups.TryGetValue(key, out var candidate))
|
||||
{
|
||||
var match = identity.ExistingMatch;
|
||||
@ -802,27 +802,6 @@ public sealed class StoryIntelligenceLocationImportService(
|
||||
|| value.Contains("occursAt", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ResolveCandidateKey(
|
||||
IReadOnlyDictionary<string, LocationCandidate> groups,
|
||||
LocationIndex existingIndex,
|
||||
string name)
|
||||
{
|
||||
var existingMatch = existingIndex.Find(name);
|
||||
if (existingMatch is not null)
|
||||
{
|
||||
var existingKey = CanonicalLocationKey(existingMatch.LocationName);
|
||||
if (groups.ContainsKey(existingKey))
|
||||
{
|
||||
return existingKey;
|
||||
}
|
||||
|
||||
return existingKey;
|
||||
}
|
||||
|
||||
var canonical = CanonicalLocationKey(name);
|
||||
return groups.ContainsKey(canonical) ? canonical : canonical;
|
||||
}
|
||||
|
||||
private static void AddResolvedName(Dictionary<string, int> map, string name, int locationId)
|
||||
{
|
||||
var clean = Clean(name);
|
||||
@ -957,6 +936,27 @@ public sealed class StoryIntelligenceLocationImportService(
|
||||
return Regex.Replace(clean, @"\s+and\s*$", string.Empty, RegexOptions.IgnoreCase).Trim();
|
||||
}
|
||||
|
||||
private static string ResolveCandidateKey(
|
||||
IReadOnlyDictionary<string, LocationCandidate> groups,
|
||||
string canonicalKey,
|
||||
string displayName)
|
||||
{
|
||||
if (groups.ContainsKey(canonicalKey))
|
||||
{
|
||||
return canonicalKey;
|
||||
}
|
||||
|
||||
if (!IsGenericLocation(displayName))
|
||||
{
|
||||
return canonicalKey;
|
||||
}
|
||||
|
||||
var contextual = groups.Keys
|
||||
.Where(key => key.EndsWith($" {canonicalKey}", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
return contextual.Count == 1 ? contextual[0] : canonicalKey;
|
||||
}
|
||||
|
||||
private static string StripLowValueContextSuffix(string value)
|
||||
{
|
||||
var clean = Clean(value);
|
||||
@ -973,6 +973,7 @@ public sealed class StoryIntelligenceLocationImportService(
|
||||
@"\s+dance\s+floor\s+and\s+bar\s+area\b.*$",
|
||||
@"\s+and\s+driveway\b.*$",
|
||||
@"\s+and\s+walk\s+to\b.*$",
|
||||
@"\s+and\s+waiting\s+area\b.*$",
|
||||
@"\s+en\s+route\b.*$",
|
||||
@"\s+and\s+outside\b.*$",
|
||||
@"\s+and\s+inside\b.*$"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user