fix(autopodbor): дубли по телефону застройщика — снят порог «людного» номера

Кейс «Образцово»: у застройщика (СМ.Сити) один номер отдела продаж легитимно
висит на всех его ЖК (277-99-01 на 9 проектах). Прежний порог dropCrowdedKeys
(>4 карточек → «рекламный», выкинуть) прятал это, и окно «Найти дубли» не
показывало проекты застройщика — systematically (6 таких номеров в данных).

Решение владельца 05.07: порог по ТЕЛЕФОНУ (p:) убрать — клиент подтверждает
склейку вручную («Это разные»), тихой ложной склейки нет; рекламные-подменыши
и так отсекаются на сборе. Порог по АДРЕСУ (a:, бизнес-центр) оставлен.
Проверено на AutoMoney: его номер у 3 карточек — все реально Автодом24, группа
верная, монстр-группы снятие порога не даёт.

Тесты переписаны со старого поведения на новое (unit + feature), 421/421.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-05 11:47:40 +03:00
parent f06b336363
commit a28058779b
3 changed files with 33 additions and 19 deletions
@@ -188,10 +188,12 @@ final class AutopodborDedup
return [];
}
// Слабые «людные» ключи выкидываем: адрес (a:) бизнес-центра и телефон (p:) — рекламный/общий
// номер, висящий у многих фирм (напр. 2ГИС-реклама одного «психа» на чужих карточках). Такой
// ключ не опознаёт «одну фирму» → по нему НЕ группируем. Сильные ключи (сайт s:/справочник d:)
// не трогаем — они опознают фирму даже у многих филиалов.
// Слабый «людный» ключ выкидываем: адрес (a:) бизнес-центра — по одному адресу реально сидят
// чужие фирмы. Телефон (p:) с порога СНЯТ (решение владельца 05.07): у застройщика один номер
// отдела продаж легитимно висит на всех его ЖК (СМ.Сити: 9 проектов на 277-99-01) — прежний
// порог 4 их прятал. Рекламные-подменыши отсекаются раньше (на сборе), а тихой ложной склейки
// нет — группу лишь ПОКАЗЫВАЕМ, склейку клиент подтверждает вручную. Сильные ключи (сайт
// s:/справочник d:) не трогаем — они опознают фирму даже у многих филиалов.
$items = $this->dropCrowdedKeys($items);
$parent = range(0, $n - 1);
@@ -249,9 +251,11 @@ final class AutopodborDedup
}
/**
* Выкидывает «людные» СЛАБЫЕ ключи, общие более чем у $cap фирм: адрес (a:) бизнес-центра и
* телефон (p:) рекламный/общий номер. Такой ключ не опознаёт «одну фирму». Сильные ключи
* (сайт s:/справочник d:) не трогаем они опознают фирму даже у многих филиалов.
* Выкидывает «людный» СЛАБЫЙ ключ-адрес (a:), общий более чем у $cap фирм: по одному адресу
* (бизнес-центр) реально сидят чужие фирмы адрес их не опознаёт как «одну фирму». Телефон (p:)
* с порога СНЯТ (решение владельца 05.07): у застройщика один номер отдела продаж легитимно висит
* на всех его ЖК режущий порог прятал их; ложной тихой склейки нет (клиент подтверждает вручную).
* Сильные ключи (сайт s:/справочник d:) не трогаем они опознают фирму даже у многих филиалов.
*
* @param array<int, array{id:int, keys?:list<string>}> $items
* @return array<int, array{id:int, keys?:list<string>}>
@@ -261,7 +265,7 @@ final class AutopodborDedup
$count = [];
foreach ($items as $it) {
foreach (array_unique($it['keys'] ?? []) as $k) {
if (str_starts_with((string) $k, 'a:') || str_starts_with((string) $k, 'p:')) {
if (str_starts_with((string) $k, 'a:')) {
$count[$k] = ($count[$k] ?? 0) + 1;
}
}
@@ -126,15 +126,20 @@ it('группа несёт сайт и ссылки-справочники ка
->and($comp['Ломбард Экспресс']['directory_urls'])->toContain('https://yandex.ru/maps/org/le/2');
});
it('предложения: рекламный номер у слишком многих (>4) НЕ группирует (порог людного номера)', function () {
it('предложения: единый номер застройщика у многих проектов ГРУППИРУЕТ (порог людного номера снят)', function () {
// Кейс «Образцово» (реш. владельца 05.07): у застройщика (СМ.Сити) один номер отдела продаж
// висит на всех его ЖК. Прежний порог 4 прятал их как «рекламный номер». Теперь показываем
// группу — склейку клиент подтверждает вручную.
[$t, $u, $run] = dupSetup();
for ($i = 1; $i <= 5; $i++) {
dupProp($t, $run, 'Фирма '.$i, ['79135396546']); // один рекламный номер у пятерых
for ($i = 1; $i <= 9; $i++) {
dupProp($t, $run, 'ЖК '.$i, ['73912779901']); // единый номер застройщика на 9 проектах
}
$this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')
$resp = $this->actingAs($u)->getJson('/api/autopodbor/field/duplicate-groups?box=proposal')
->assertOk()
->assertJsonCount(0, 'groups');
->assertJsonCount(1, 'groups');
expect($resp->json('groups.0.competitors'))->toHaveCount(9);
expect(mb_strtolower($resp->json('groups.0.reason')))->toContain('телефон');
});
it('GET field/duplicate-groups — группирует по общему адресу источников (слабый сигнал)', function () {
@@ -271,16 +271,21 @@ it('duplicateGroups: защита от бизнес-центра — адрес
expect($groups)->toBe([]);
});
it('duplicateGroups: рекламный/общий телефон у слишком многих фирм НЕ группирует (как бизнес-центр)', function () {
// Номер «психа» AutoMoney (реклама) или общая 8-800-линия висит у 6 фирм (порог 4) → это НЕ
// «одна фирма», а рекламный/общий номер → такой телефон-ключ выкидываем, ложную группу не строим.
it('duplicateGroups: телефон у многих фирм ГРУППИРУЕТ — решение владельца (клиент подтвердит сам)', function () {
// Решение владельца 05.07: порог «людного» телефона УБРАН. У застройщика один номер отдела
// продаж легитимно висит на всех его ЖК (СМ.Сити: 9 проектов на 277-99-01) — старый порог 4
// прятал это. Рекламные-подменыши отсекаются раньше (на сборе), а тихой ложной склейки нет:
// группу лишь ПОКАЗЫВАЕМ, склейку клиент подтверждает вручную («Это разные»). Проверено на
// AutoMoney: его номер висит у 3 карточек — все реально Автодом24, группа верная.
$items = [];
for ($i = 1; $i <= 6; $i++) {
$items[] = ['id' => $i, 'keys' => ['p:79135396546']];
for ($i = 1; $i <= 9; $i++) {
$items[] = ['id' => $i, 'keys' => ['p:73912779901']];
}
$groups = mergeService()->duplicateGroups($items);
expect($groups)->toBe([]);
expect($groups)->toHaveCount(1);
expect($groups[0]['ids'])->toBe([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(mb_strtolower($groups[0]['reason']))->toContain('телефон');
});
it('duplicateGroups: общий телефон у немногих (2 фирмы) ОСТАЁТСЯ — идёт на решение клиента', function () {