@@ -85,8 +85,11 @@ class SpamHeuristics
8585 fun isNeighborSpoof (
8686 context : Context ,
8787 incomingNumber : String ,
88+ numberingPlan : NumberingPlan = NumberingPlan .from(incomingNumber),
8889 ): Boolean {
90+ if (numberingPlan != NumberingPlan .NANP ) return false
8991 val userNumber = getUserPhoneNumber(context) ? : return false
92+ if (NumberingPlan .from(userNumber) != NumberingPlan .NANP ) return false
9093 val userDigits = filterAsciiDigitsLast(userNumber, 10 )
9194 val inDigits = filterAsciiDigitsLast(incomingNumber, 10 )
9295
@@ -101,7 +104,11 @@ class SpamHeuristics
101104 // Toll-free numbers are heavily abused by robocallers
102105 private val tollFreePrefixes = setOf (" 800" , " 888" , " 877" , " 866" , " 855" , " 844" , " 833" )
103106
104- fun isTollFree (number : String ): Boolean {
107+ fun isTollFree (
108+ number : String ,
109+ numberingPlan : NumberingPlan = NumberingPlan .from(number),
110+ ): Boolean {
111+ if (numberingPlan != NumberingPlan .NANP ) return false
105112 val digits = filterAsciiDigitsLast(number, 10 )
106113 if (digits.length < 10 ) return false
107114 return digits.substring(0 , 3 ) in tollFreePrefixes
@@ -322,7 +329,11 @@ class SpamHeuristics
322329 " 437373" ,
323330 )
324331
325- fun isHighSpamVoipRange (number : String ): Boolean {
332+ fun isHighSpamVoipRange (
333+ number : String ,
334+ numberingPlan : NumberingPlan = NumberingPlan .from(number),
335+ ): Boolean {
336+ if (numberingPlan != NumberingPlan .NANP ) return false
326337 val digits = filterAsciiDigitsLast(number, 10 )
327338 if (digits.length < 10 ) return false
328339 return digits.substring(0 , 6 ) in highSpamVoipNpanxx
@@ -341,7 +352,11 @@ class SpamHeuristics
341352
342353 fun hasHotRanges (): Boolean = hotCampaignRanges.isNotEmpty()
343354
344- fun isHotCampaignRange (number : String ): Boolean {
355+ fun isHotCampaignRange (
356+ number : String ,
357+ numberingPlan : NumberingPlan = NumberingPlan .from(number),
358+ ): Boolean {
359+ if (numberingPlan != NumberingPlan .NANP ) return false
345360 if (hotCampaignRanges.isEmpty()) return false
346361 val digits = filterAsciiDigitsLast(number, 10 )
347362 if (digits.length < 6 ) return false
@@ -370,12 +385,15 @@ class SpamHeuristics
370385 number : String ,
371386 windowMs : Long = 3600_000,
372387 threshold : Int = 3,
388+ numberingPlan : NumberingPlan = NumberingPlan .from(number),
373389 ): Boolean {
390+ if (numberingPlan != NumberingPlan .NANP ) return false
374391 val now = System .currentTimeMillis()
375392 val normalized = filterAsciiDigitsLast(number, 10 )
376393 val count =
377394 recentNumbers.count { (num, time) ->
378- filterAsciiDigitsLast(num, 10 ) == normalized && (now - time) < windowMs
395+ NumberingPlan .from(num) == NumberingPlan .NANP &&
396+ filterAsciiDigitsLast(num, 10 ) == normalized && (now - time) < windowMs
379397 }
380398 return count >= threshold
381399 }
@@ -393,6 +411,7 @@ class SpamHeuristics
393411 fun analyze (
394412 context : Context ,
395413 number : String ,
414+ numberingPlan : NumberingPlan = NumberingPlan .from(number),
396415 smsBody : String? = null,
397416 recentBlockedNumbers : List <Pair <String , Long >> = emptyList(),
398417 enableNeighborSpoof : Boolean = true,
@@ -418,31 +437,31 @@ class SpamHeuristics
418437 }
419438
420439 // High-spam VoIP range
421- if (isHighSpamVoipRange(number)) {
440+ if (isHighSpamVoipRange(number, numberingPlan )) {
422441 score + = 30
423442 reasons.add(" voip_spam_range" )
424443 }
425444
426445 // Active campaign range — 3+ distinct numbers from this NPA-NXX in last 24h
427- if (isHotCampaignRange(number)) {
446+ if (isHotCampaignRange(number, numberingPlan )) {
428447 score + = 35
429448 reasons.add(" hot_campaign_range" )
430449 }
431450
432451 // Toll-free (mild signal — many legit businesses use these)
433- if (isTollFree(number)) {
452+ if (isTollFree(number, numberingPlan )) {
434453 score + = 10
435454 reasons.add(" toll_free" )
436455 }
437456
438457 // Neighbor spoofing (gated by the Settings toggle)
439- if (enableNeighborSpoof && isNeighborSpoof(context, number)) {
458+ if (enableNeighborSpoof && isNeighborSpoof(context, number, numberingPlan )) {
440459 score + = 50
441460 reasons.add(" neighbor_spoof" )
442461 }
443462
444463 // Rapid-fire calling
445- if (isRapidFire(recentBlockedNumbers, number)) {
464+ if (isRapidFire(recentBlockedNumbers, number, numberingPlan = numberingPlan )) {
446465 score + = 40
447466 reasons.add(" rapid_fire" )
448467 }
@@ -531,6 +550,13 @@ class SpamHeuristics
531550 smsBody : String? = null,
532551 recentBlockedNumbers : List <Pair <String , Long >> = emptyList(),
533552 senderProvenance : SenderProvenance ? = null,
534- ): HeuristicResult = shared.analyze(context, number, smsBody, recentBlockedNumbers, senderProvenance = senderProvenance)
553+ ): HeuristicResult =
554+ shared.analyze(
555+ context,
556+ number,
557+ smsBody = smsBody,
558+ recentBlockedNumbers = recentBlockedNumbers,
559+ senderProvenance = senderProvenance,
560+ )
535561 }
536562 }
0 commit comments