» /rd/lib/Phone/Prefix.php

<?
/**
 *  Synopsis - phone number prefixes facility. Validation, information etc.
 *  Information about 3 digit prefixes and 3+3 digit prefixes.
 * 
 *  
 */

class Phone_Prefix {
    
//////////////////////////////////////////
    ///
    ///  Static data
    ///
    //////////////////////////////////////////

    /**
     * Get list of Area Codes in $state
     * @param string $state - 2 chars ST
     * @return int[]
     * @example Phone_Prefix::stateCodes("MA")
     */
    
static function stateCodes(string $state) {
        return @
Phone_PrefixAdmin::$STATE2AREACODE[Phone_PrefixAdmin::COUNTRY_CODE_US][$state];
    }


/*
    static function fix202Prefixes() {
        $loc = [
           "region" => 51,
           "city_main" => 1,
           "rc" => 3342337,
           "city" => 1,
           "rm" => 3342337,
           "geo" => [
             38.9068429, -77.0058123
           ],
           "state" => "DC",
           "region_name" => "DC",
           "city_name" => "Washington",
           "enriched" => 16,
           "county_id" => 2062,
           "county" => "District of Columbia",
           "county_name" => "District of Columbia"];

        $list = M("geo.phone")->findA(["_id" => ['$gte' => 202000, '$lte' => 202999]]);
        foreach ($list as $rec) {
            M("geo.phone")->upsertSet((int)$rec["_id"], ["loc" => $loc]);
        }
        $list = M("geo.phone")->findA(["_id" => ['$gte' => 202, '$lte' => 202]]);
        foreach ($list as $rec) {
            M("geo.phone")->upsertSet((int)$rec["_id"], ["loc" => $loc]);
        }
    }


    static function fixPrefix6Records() {
        ini_set("memory_limit", -1);
        $list = M("geo.phone")->findA(["_id" => ['$gt' => 200000]]);
        $updated = $processed = 0;

        foreach ($list as $rec) {
            $update_required = false;
            if ($usage = @$rec["details"]["usage"]) {
                $d = [];
                if ($usage == "Landline") {
                    $d["type"] = "landline";
                } elseif ($usage == "Unknown") {
                    //$d["type"] = "Unknown";
                } elseif ($usage == "Cell Number") {
                    $d["type"] = "wireless";
                } else {
                    vd($rec);
                }

                if ($rec["details"]["company"]) {
                    $d["carrier"] = $rec["details"]["company"];
                }
                if ($rec["details"]["introduced"]) {
                    $d["introduced"] = $rec["details"]["introduced"];
                }
                $rec["details"] = $d;
            }
            if (@$rec["loc"]["city"] && !@$rec["loc"]["city_name"]) {
                $new_loc = \Geo::parseLoc($rec["loc"], \Geo::LOC_ENRICH_ALL);
                if ($new_loc["rc"]) {
                    $rec["loc"] = $new_loc;
                    $update_required = true;
                }
            }
            $processed++;
            if ($update_required) {
                $updated++;
                if (!@$rec["loc"] && !$rec["details"]) {
                    vd("Empty record: ", $rec);
                }
echo("\n$processed/$updated");
                M("geo.phone")->upsertSet((int)$rec["_id"], ["loc" => $rec["loc"], "details" => $rec["details"]]);
            }
        }
        return $list;
    }
*/

    /**
     * Extract all possible "timezone" field values from allareacodes.com data
     * @return array
     * @example Phone_Prefix::listAllTimezones()
     */
/*
    static function listAllTimezones() {
        $list = [];
        for ($prefix = 201; $prefix<=999; $prefix++) {
            if ($prefix_info = self::prefix3($prefix) ) {
                @$list[$prefix_info["details"]["timezone"]]["cnt"]++;
                @$list[$prefix_info["details"]["timezone"]]["prefixes"][] = $prefix;
            }
        }
        return $list;
    }
*/

    //////////////////////////////////////////
    ///
    ///  Mongo collection from allareacodes.com methods
    ///  See: /rd/lib.framework/Geo/PhoneAdmin.php - data collection/update script
    ///
    //////////////////////////////////////////

    /**
     * Get prefix-3 info
     * @param string|int $phone at least 3 digits of US phone number WITHOUT leading 1.
     * @return array|object|null
     * @example Phone_Prefix::prefix3("781-204-4673")
     *          Phone_Prefix::prefix3("1 781-204-4673")
     *          Phone_Prefix::prefix3("781")
     */
    
static function prefix3($phone) {
        return 
self::_prefix($phone3);
    }

    
/**
     * Get prefix-6 info
     * @param string|int $phone at least 6 digits of US phone number WITHOUT leading 1.
     * @return array|object|null
     * @example Phone_Prefix::prefix6("781-204-4673")
     *          Phone_Prefix::prefix6("1 781-204-4673")
     *          Phone_Prefix::prefix6(781204)
     */
    
static function prefix6($phone) {
        return 
self::_prefix($phone6);
    }

    static function 
_prefix($phone$length=3) {
        
$phone Phone::clean($phone);
        
$prefix substr(self::_no1($phone), 0$length);
        
$res M("geo.phone")->findOne((int)$prefix);

        if (
$res) {
            if (empty(
$res["loc"]["rc"])) {
                if (
$tmp = ($res["details"]["top_cities"]??'')) {
                    
$tmp reset($tmp);
                    
$res["loc"] = $tmp["loc"];
                } elseif (
$res["loc"]["rm"]??0) {
                    
$res["loc"]["rc"] = $res["loc"]["rm"];
                    list(
$res["loc"]["city"], $res["loc"]["region"]) = Geo::rc($res["loc"]["rc"]);
                    
$res["loc"]["city_name"] = Geo::city_name($res["loc"]["region"], $res["loc"]["city"], false);
                }
            }
        } else {
            
// No information on prefix $prefix found in mongo
            
$res = [];
        }
/*
{
   "_id" => 202),
   "loc": {
     "region" => 47),
     "rm" => 3080192▼),
     "state": "WA",
     "region_name": "Washington"
  },
   "details": {
     "timezone": "Eastern",
     "is_first_86" => 1),
     "nearby_prefixes": [
       {
         "loc": {
           "region" => 20),
           "city_main" => 214),
           "rc" => 1310934),
           "city" => 214),
           "rm" => 1310934),
           "geo": [
             39.1731621,
             -77.2716502
          ],
           "state": "MD",
           "region_name": "Maryland",
           "city_name": "Germantown",
           "enriched" => 16),
           "county_id" => 1824),
           "county": "Montgomery",
           "county_name": "Montgomery County"
        },
         "prefixes": [
           "240",
           "301"
        ]
      },
       {
         "loc": {
           "region" => 20),
           "city_main" => 286),
           "rc" => 1311006),
           "city" => 286),
           "rm" => 1311006),
           "geo": [
             39.282544,
             -76.6201184
          ],
           "state": "MD",
           "region_name": "Maryland",
           "city_name": "Baltimore",
           "enriched" => 16),
           "county_id" => 3228),
           "county": "Baltimore City",
           "county_name": "Baltimore City County"
        },
         "prefixes": [
           "410",
           "443",
           "667"
        ]
      },
       {
         "loc": {
           "region" => 45),
           "city_main" => 829),
           "rc" => 2949949),
           "city" => 829),
           "rm" => 2949949),
           "geo": [
             37.27,
             -79.94
          ],
           "state": "VA",
           "region_name": "Virginia",
           "city_name": "Roanoke",
           "enriched" => 16),
           "county_id" => 3232),
           "county": "Roanoke City",
           "county_name": "Roanoke City County"
        },
         "prefixes": [
           "540"
        ]
      },
       {
         "loc": {
           "region" => 45),
           "city_main" => 106),
           "rc" => 2949226),
           "city" => 106),
           "rm" => 2949226),
           "geo": [
             38.8011056,
             -77.0703853
          ],
           "state": "VA",
           "region_name": "Virginia",
           "city_name": "Arlington",
           "enriched" => 16),
           "county_id" => 1939),
           "county": "Arlington",
           "county_name": "Arlington County"
        },
         "prefixes": [
           "571",
           "703"
        ]
      }
    ],
     "top_counties": [
       {
         "loc": {
           "region" => 51),
           "rm" => 3342336),
           "county_id" => 2062),
           "state": "DC",
           "region_name": "DC",
           "enriched" => 16),
           "county": "District of Columbia",
           "county_name": "District of Columbia"
        },
         "population": "601723",
         "percent_of_prefix": "100",
         "percent_of_county": "100"
      }
    ],
     "top_cities": [
       {
         "loc": {
           "region" => 51),
           "city_main" => 1),
           "rc" => 3342337),
           "city" => 1),
           "rm" => 3342337),
           "geo": [
             38.9068429,
             -77.0058123
          ],
           "state": "DC",
           "region_name": "DC",
           "city_name": "Washington",
           "enriched" => 16),
           "county_id" => 2062),
           "county": "District of Columbia",
           "county_name": "District of Columbia"
        },
         "population": "601723",
         "percent_of_prefix": "100",
         "percent_of_city": "100"
      }
    ],
     "top_carriers": [
       {
         "carrier": "Verizon",
         "type": "landline",
         "percent_of_prefix": "49"
      },
       {
         "carrier": "Xo Dc",
         "type": "landline",
         "percent_of_prefix": "2"
      },
       {
         "carrier": "Bandwidthcom Clec",
         "type": "landline",
         "percent_of_prefix": "2"
      },
       {
         "carrier": "Teleport America",
         "type": "landline",
         "percent_of_prefix": "2"
      },
       {
         "carrier": "Mcimetro Access Transmission Services",
         "type": "landline",
         "percent_of_prefix": "2"
      },
       {
         "carrier": "Level 3",
         "type": "landline",
         "percent_of_prefix": "2"
      },
       {
         "carrier": "Paetec",
         "type": "landline",
         "percent_of_prefix": "1"
      },
       {
         "carrier": "Other",
         "type": "landline",
         "percent_of_prefix": "8"
      },
       {
         "carrier": "Verizon Wireless",
         "type": "wireless",
         "percent_of_prefix": "10"
      },
       {
         "carrier": "Sprint",
         "type": "wireless",
         "percent_of_prefix": "7"
      },
       {
         "carrier": "Cingular",
         "type": "wireless",
         "percent_of_prefix": "6"
      },
       {
         "carrier": "Omnipoint Cap Operations",
         "type": "wireless",
         "percent_of_prefix": "3"
      },
       {
         "carrier": "Usa Mobility Wireless",
         "type": "wireless",
         "percent_of_prefix": "3"
      },
       {
         "carrier": "Verizon",
         "type": "wireless",
         "percent_of_prefix": "3"
      },
       {
         "carrier": "Cricket Comm",
         "type": "wireless",
         "percent_of_prefix": "1"
      },
       {
         "carrier": "Other",
         "type": "wireless",
         "percent_of_prefix": "1"
      }
    ],
     "assigned_percent": "84",
     "landline_prefix6_cnt" => 439),
     "wireless_prefix6_cnt" => 221),
     "unknown_prefix6_cnt" => 3)
  }
}
 */
        
return $res;
    }

    
/**
     * List of prefix3 records
     * @param array $criteria
     *          "region"
     *          "city"
     *          "city_main"
     *          "county_id"
     * @return array
     * @example Phone_Prefix::prefixes3list(["region" => 19])
     */
    
static function prefixes3list($criteria = []) {
        
$q = ["_id" => ['$lt' => 1000]];
        if (!empty(
$criteria["region"])) {
            
$q["loc.region"] = (int) $criteria["region"];
            if (!empty(
$criteria["city"])) {
                
$q["loc.county_id"] = (int)$criteria["city"];
            } elseif (!empty(
$criteria["city_main"])) {
                
$q["loc.city_main"] = (int)$criteria["city_main"];
            }
        }
        if (!empty(
$criteria["county_id"])) {
            
$q["loc.county_id"] = (int)$criteria["county_id"];
        }
        return 
M("geo.phone")->findA($q);
    }

    
/**
     * List of prefix6 records
     * @param array $criteria
     *          "prefix3"
     *          "region"
     *          "city"
     *          "city_main"
     *          "county_id"
     * @return array
     * @example Phone_Prefix::prefixes6list(["prefix3" => 781])
     */
    
static function prefixes6list($criteria = []) {
        
$q = ["_id" => ['$gt' => 1000]];
        if (
$prefix3 = (int)($criteria["prefix3"] ?? 0)) {
            
$q["_id"] = ['$gte' => $prefix3 1000'$lt' => $prefix3 1000 1000];
        }
        if (
$criteria["region"]??0) {
            
$q["loc.region"] = (int) $criteria["region"];
            if (
$criteria["county_id"]??0) {
                
$q["loc.county_id"] = (int)$criteria["county_id"];
            }
            if (
$criteria["city"]??0) {
                
$q["loc.city"] = (int)$criteria["city"];
            } elseif (
$criteria["city_main"]??0) {
                
$q["loc.city_main"] = (int)$criteria["city_main"];
            }
        }
        return 
M("geo.phone")->findA($q);
    }



    
/**
     * @param $phone
     * @param bool $as_string
     * @return array|bool|bool[]
     * @example Phone_Prefix::location("1 (617) 123-4567")
     */
    
static function location($phone$as_string false){ # false | loc structure | location string

        
$p = (string)M_Type::apply($phone"phone");

        if (
strlen($p) == 11 && $p[0] == "1") {
            
$p substr($p1);
        } else {
            
//Bad formatted or non-US phone
            
return false;
        }
        
$d3 substr($p03);
        
$d6 substr($p06);

        
$loc6 M("geo.phone")->findOne((int)$d6);
        
$loc3 M("geo.phone")->findOne((int)$d3);

        
$loc = (array)($loc6["loc"] ?? []) + (array)($loc3["loc"] ?? []);
        if (
$loc) {
            return 
$as_string?HB::loc2str($loc):($loc+["range" => true]);
        }
        return 
false;
    }

    
//////////////////////////////////////////
    ///
    ///  Stats methods
    ///
    //////////////////////////////////////////

    /**
     * Return prefix3 distribution in $city_id or $county_id
     * @param int $region
     * @param int $county_id
     * @param int $city_id
     * @return array [<area_code> => <percent>]. <area_code> is XXX, XXX / YYY ( for overlay codes ) or "Other"
     */
    
static function prefix3Distribution(int $regionint $county_idint $city_id 0) {
        
$distribution = [];
        if (
$p3list Phone_Prefix::prefixes3list(["region" => $region])) {

            foreach (
$p3list as $p3 => $p3info) {
                if (
$county_id) {
                    if (!empty(
$p3info["details"]["top_counties"])) {
                        foreach (
$p3info["details"]["top_counties"] as $top_county) {
                            if ((
$top_county["loc"]["county_id"] ?? null) === $county_id) {
                                if (!empty(
$p3info["details"]["overlaid_by"])) {
                                    if (
is_array($p3info["details"]["overlaid_by"])) {
                                        
$pkey $p3 " / " join(" / "$p3info["details"]["overlaid_by"]);
                                    } else {
                                        
$pkey $p3 " / " $p3info["details"]["overlaid_by"];
                                    }
                                } elseif (!empty(
$p3info["details"]["overlay_for"])) {
                                    
$pkey $p3info["details"]["overlay_for"] . " / " $p3;
                                } else {
                                    
$pkey $p3;
                                }
                                
$distribution[$pkey] = $top_county["percent_of_county"];
                            }
                        }
                    }
                } elseif (
$city_id) {
                    if (!empty(
$p3info["details"]["top_cities"])) {
                        foreach (
$p3info["details"]["top_cities"] as $top_city) {
                            if ((
$top_city["loc"]["city"] ?? 0) == $city_id) {
                                if (!empty(
$p3info["details"]["overlaid_by"])) {
                                    if (
is_array($p3info["details"]["overlaid_by"])) {
                                        
$pkey $p3 " / " implode(" / "$p3info["details"]["overlaid_by"]);
                                    } else {
                                        
$pkey $p3 " / " $p3info["details"]["overlaid_by"];
                                    }
                                } elseif (!empty(
$p3info["details"]["overlay_for"])) {
                                    
$pkey $p3info["details"]["overlay_for"] . " / " $p3;
                                } else {
                                    
$pkey $p3;
                                }
                                
$distribution[$pkey] = $top_city["percent_of_city"];
                            }
                        }
                    }
                }
            }
        }
        
$sum 0;
        
$clean = [];
        foreach (
$distribution as $pkey => $percent) {
            
$pcanonical explode(" / "$pkey)[0];
            if (empty(
$clean[$pcanonical])) {
                
$sum += $percent;
                
$clean[$pcanonical] = 1;
            } else {
                unset(
$distribution[$pkey]);
            }
        }

        if (
$sum 100) {
            
$distribution["Other"] = (100 $sum);
        }

        return 
$distribution;
    }

    
//////////////////////////////////////////
    ///
    ///  KRDB_Phone based methods
    ///
    //////////////////////////////////////////

    /**
     * @param type $prefix
     * @return type
     */
    
static function info7($prefix) {
        
//fetch prefix 6 info
        
return self::info6($prefix);
    }
    
    static function 
info6($prefix) {
        
$prefix self::_no1($prefix);
        
$prefix6 substr($prefix06);
        
$prefix7 KRDB::i('phone'"1".$prefix6)->info;
        if (
$prefix7["zips"]??[]) {
            foreach (
$prefix7["zips"] as $k=>$v){
                if (!(int)
$v) {
                    unset(
$prefix7["zips"][$k]);
                }
            }
        }
        return 
$prefix7;
    }
    
    static function 
info3($prefix) {
        
$prefix self::_no1($prefix);
        
$prefix3 substr($prefix03);
        return 
KRDB::i('phone'"1".$prefix3)->info;
    }
    
    static function 
_no1($phone) {
        
$phone "".$phone;
        if ((
strlen($phone) == 11) && ($phone[0] == "1")) {
            
$phone substr($phone1);
        }
        return 
$phone;
    }
    
    static function 
cityByPrefix($prefix_info) {
        
$pfx_city "";
        
        if (
$locs $prefix_info["locations"] ?? null) {

            
//remove locations without phones
            
foreach ($locs as $lid=>$loc) {
                if (empty(
$loc["phones_cnt"]) && empty($loc["primary"])) {
                    unset(
$locs[$lid]);
                }
            }

            
$locs AH::rsort($locs"primary""phones_cnt");
            foreach (
$locs as $loc) {
                
$city_name $loc["city_name"].cs(", %s"NVL($loc["state"] ?? null$loc["state_name"] ?? null));
                if (!empty(
$loc["primary"])) {

                    $pfx_city $city_name cs("&nbsp;(".number_format($loc["phones_cnt"]??0).")"$loc["phones_cnt"]??0);

                }
            }
        }
        
        return 
$pfx_city;
    }
}