Every column in the download, with its type, a real example value and how much of it is actually populated — measured over all 562,060 rows of the current release, not quoted from a specification sheet.
NPANXX + ZipCodeUnlike the ZIP+4 product, this one is not split by state — the whole numbering plan fits comfortably in a single file. You get one archive per format, so take the one your stack wants and ignore the rest.
| File | What it is | Approx. size | Edition |
|---|---|---|---|
YYYY-M-DeluxeAreaCodeDatabase-csv.zip | All 31 columns, comma-separated | 9.5 MB | Deluxe |
YYYY-M-DeluxeAreaCodeDatabase-tab.zip | The same table, tab-delimited | 9.4 MB | Deluxe |
YYYY-M-DeluxeAreaCodeDatabase-mdb.zip | The same table as a Microsoft Access database | 22 MB | Deluxe |
YYYY-M-StandardAreaCodeDatabase-csv.zip | All 9 columns, comma-separated | 3.0 MB | Standard |
YYYY-M-StandardAreaCodeDatabase-tab.zip | The same table, tab-delimited | 3.0 MB | Standard |
YYYY-M-StandardAreaCodeDatabase-mdb.zip | The same table as a Microsoft Access database | 7.3 MB | Standard |
AreaCode-NPANXX-Database-Specifications-Edition.pdf | The specification sheet | — | All |
Uncompressed, it is a great deal bigger. The Deluxe CSV expands to about 110 MB and the Access file to roughly 200 MB; Standard is around 19 MB and 71 MB. All three formats of an edition carry identical data — the same columns, the same rows, the same values — so pick on convenience, not on content.
The file name carries the publication month, so 2026-6-DeluxeAreaCodeDatabase-csv.zip
is the June 2026 build. Sizes measured from the
June2026 release.
Every column in the download, with the exact name as it appears in the file header. 9 in Standard, 31 in Deluxe. Standard is a strict subset — every Standard column is present in Deluxe under the same name.
One row is an NPA-NXX and a ZIP Code, not just an exchange.
An exchange usually reaches several ZIP Codes, so it appears once per ZIP Code
it serves. ZipCodeFreq tells you which of those is the main one.
If you want one row per exchange, filter to the highest frequency rather than
assuming NPANXX is unique — across 562,061 rows there are
far fewer distinct exchanges than rows.
The file carries two different coordinates, and they answer different
questions. Latitude / Longitude place the
central office — the switch. WeightedLat /
WeightedLon place the centroid of the ZIP Code the exchange most
often serves — the people. For mapping customers, routing leads or
drawing territories, the weighted pair is almost always the one you want.
Two columns ship empty, and we would rather say so than let you find out.
SwitchCLLI was withdrawn by the source — the specification
sheet has documented it as blank for years — and NPANew only
carries a value while an area code split is in its permissive-dialing period,
of which there is none in this release. Both are still in the layout so existing
importers keep working. They are marked below.
| Column name | Description | Type | Standard | Deluxe |
|---|---|---|---|---|
| The NPA-NXX code itself 4 fields | 4 | 4 | ||
NPA | Area code — the Numbering Plan Area, the first three digits of a North American phone number.Example: 201 | Char(3) | ||
NXX | The three-digit central office prefix that follows the area code (X=2-9, X=0-9), generally 201 through 999.Example: 200 | Char(3) | ||
NPANXX | The NPA and NXX joined into the single six-digit code most systems key on.Example: 201200 | Char(6) | ||
LATA | Local Access and Transport Area — the regulatory calling region the exchange sits in.Example: 224 | VarChar(255) | ||
| ZIP Code linkage 3 fields | 3 | 3 | ||
ZipCode | The five-digit ZIP Code associated with this NPA-NXX. An exchange can span several ZIP Codes, so an NPA-NXX appears once per ZIP Code it reaches — see ZipCodeFreq.Example: 07304 | VarChar(255) | ||
ZipCodeCount | Approximate population of that ZIP Code.Example: 41233 | Integer | ||
ZipCodeFreq | How often this ZIP Code occurs within the NPA-NXX, as a percentage. Three values are sentinels rather than percentages: 0 unknown, -1 the most frequent ZIP Code but the share is unknown, -2 the second most frequent, share unknown.Example: 0 | Integer | ||
| Where the exchange is 7 fields Deluxe adds 6 | 1 | 7 | ||
State | Two-letter state, province or territory code.Example: NJ | Char(2) | ||
City | Popular or political name of the city served.Example: JERSEY CITY | VarChar(255) | — | |
County | County name.Example: HUDSON | VarChar(255) | — | |
CountyPop | datedEstimated county population, to the nearest thousand.Dated. The specification sheet dates this estimate to 1990 census data. It is kept for backward compatibility; use a current county population source if the number matters. | Integer | — | |
FIPS | Census Bureau FIPS code for the county of the central office. Ships with its leading zero — read it as text.Example: 34017 | VarChar(255) | — | |
MSA_CBSA | Census Bureau name of the metropolitan or micropolitan statistical area.Example: New Haven, CT | VarChar(255) | — | |
MSA_CBSA_CODE | Census Bureau CBSA code for that statistical area.Example: 35620 | VarChar(255) | — | |
| Coordinates 4 fields Deluxe only | 0 | 4 | ||
Latitude | Latitude of the central office serving this exchange.Example: 40.717500 | Decimal(12,8) | — | |
Longitude | Longitude of the central office serving this exchange.Example: -74.072200 | Decimal(12,8) | — | |
WeightedLat | Latitude of the centroid of the ZIP Code that occurs most frequently within this NPA-NXX. This is the coordinate to use when you want to place the population an exchange serves rather than the switch equipment.Example: 40.719134 | Decimal(12,8) | — | |
WeightedLon | Longitude of that same weighted centroid.Example: -74.033915 | Decimal(12,8) | — | |
| Time zone 2 fields Deluxe only | 0 | 2 | ||
TimeZone | Offset from GMT as a positive number of hours behind: 4 Atlantic, 5 Eastern, 6 Central, 7 Mountain, 8 Pacific, 9 Alaska, 10 Hawaii-Aleutian, with 11, 14 and 20 covering the Pacific NANP territories.Example: 5 | VarChar(255) | — | |
ObservesDST | Whether the area observes Daylight Saving Time. Arizona outside the Navajo Nation, Hawaii, Guam, Puerto Rico, the Virgin Islands and parts of Indiana do not.Example: Y | VarChar(255) | — | |
| Carrier & rate center 6 fields Deluxe only | 0 | 6 | ||
OCN | Operating Company Number — the four-character code identifying the carrier holding the code. Alphanumeric and can lead with a zero, so read it as text.Example: 9206 | VarChar(255) | — | |
Company | Name of that carrier.Example: VERIZON NEW JERSEY, INC. | VarChar(255) | — | |
RateCenter | Ten-character rate center name for the central office. Blank where the rate center is unknown.Example: JERSEYCITY | VarChar(255) | — | |
CoverageAreaName | Name of the area this NPA-NXX covers — usually the city, sometimes an alias.Example: JERSEY CITY | VarChar(255) | — | |
NXXUseType | Primary use of the exchange: L landline, W wireless (cellular, PCS, paging). Some exchanges are split between the two; this is the reported type, not a guarantee.Example: L | VarChar(255) | — | |
SwitchCLLI | always emptyCommon Language Location Identifier for the switch.Empty. The specification sheet says this field “is now blank and will be removed in a future version”, and that is what ships: 0 of 562,061 rows carry a value. It stays in the layout so existing importers do not break. | VarChar(255) | — | |
| Status & lifecycle 4 fields Deluxe only | 0 | 4 | ||
Status | Whether the code is in active use: Active or Inactive. Both ship — filter if you only want live exchanges.Example: Active | VarChar(35) | — | |
NXXIntroVersion | The release in which this NXX first appeared, as a date. Useful for spotting recently opened exchanges.Example: 1998-01-25 | VarChar(255) | — | |
Overlay | L marks an area code that is a recent overlay on an existing geographic area; blank means it is not an overlay.Example: L | VarChar(255) | — | |
NPANew | always emptyDuring an area code split's permissive-dialing period, the NEW area code, while the NPA column still shows the old one.Empty. Populated only while an area code split is in its permissive-dialing period. There is no split in flight in this release, so the column is empty in all 562,061 rows. Do not treat an empty value as “no split ever” in historical work. | VarChar(255) | — | |
| Record flags 1 field | 1 | 1 | ||
Flags | sparseFlags describing something unusual about the record.Sparse. Set on only 190 of 562,061 rows (0.03%). S means the state on this record does not match the state of the ZIP Code, and the area code’s state was used. | VarChar(35) | ||
Column names are read from the shipped CSV header, types and descriptions from the specification sheet included with every download, and examples are real values out of the file. Measured from publication 2026-06 — 562,061 rows in Deluxe, 562,060 in Standard. Both editions ship as one file covering the whole numbering plan, in CSV, tab-delimited and Access formats.
Three formats, identical content. Every edition ships all three at no extra cost, so there is no decision to regret here.
, comma" double quote, used where a value contains a comma — Company and MSA_CBSA both doCRLF (\r\n)UTF-8 with a byte-order mark0x09)CRLFUTF-8 with a byte-order markDeluxeAreaCodeDatabaseEF BB BF. This is the only product in
our range that ships one.\uFEFFState (Standard) or \uFEFFNPA (Deluxe), and every
lookup on that column silently misses.encoding="utf-8-sig". .NET:
new StreamReader(path, Encoding.UTF8, true). Java and PHP do
not strip it — remove it from the header row yourself.NPANXX is not a primary keyNPANXX + ZipCode. Declaring
NPANXX as the primary key fails the import on the second row.ZipCode and FIPS both carry significant leading zeros
— 07311, 06001.OCN is alphanumeric (624H, 325E) and can also
lead with a zero.ZipCodeFreq is a percentage, except -1 (most frequent,
share unknown), -2 (second most frequent) and 0
(unknown). Sorting on the raw number puts the best row last.Latitude/Longitude of 0.000000 is a
missing value, not a point in the Gulf of Guinea. About 17% of rows are unresolved.ObservesDST ships Y/N, though the
specification sheet documents 1/0.SwitchCLLI and NPANew are empty in every row of this
release. They remain in the layout for compatibility.LOAD DATA LOCAL INFILE '2026-6-DeluxeAreaCodeDatabase.csv' INTO TABLE npanxx CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\r\n' IGNORE 1 ROWS;COPY npanxx FROM '/path/to/file.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8'); — HEADER true discards the BOM with the header line.BULK INSERT npanxx FROM 'C:\path\to\file.csv' WITH (FORMAT='CSV', FIRSTROW=2, FIELDTERMINATOR=',', ROWTERMINATOR='0x0d0a', CODEPAGE='65001');Company and
MSA_CBSA are where most import failures start.NPANXX, NPA and State. Add
OCN if you report by carrier.VARCHAR, never
INT.200 real records per edition, taken from the current production file, in all three formats with that edition’s specification sheet. Test your importer against the actual layout — including the BOM — before you buy. No email required.
31 columns · 200 records
The full layout: carrier and OCN, rate center, landline/wireless, both coordinate pairs, city, county, FIPS, CBSA, time zone and status.
CSV · TAB · MDB
Download sample 226 KB9 columns · 200 records
The code, the state, the LATA and the ZIP Code correlation with its count and frequency ranking.
CSV · TAB · MDB
Download sample 202 KBBoth samples were regenerated from the June2026 production file, so the column list and the quirks match exactly what you would receive. Sizes measured from the archives themselves.
Reading the sample first is the point. Every trap on this page — the BOM, the
repeated NPANXX, the sentinel frequencies — is reproducible in these
200 rows.
20 real questions from developers and data analysts working with this data. We have been answering them since 2003, so these are the ones that come up again and again — why NPANXX is not a unique key, what the sentinel values in ZipCodeFreq actually mean, and how to handle the byte-order mark on the first column. Answers, not marketing.
The Area Code Database (also known as the NPA-NXX Database) is a comprehensive electronic dataset containing all valid telephone numbering assignments across the North American Numbering Plan. The database includes approximately 558,000 NPA-NXX records covering the United States, U.S. territories, and Canadian reference data.
NPA-NXX Explained: NPA (Numbering Plan Area) is the 3-digit area code, and NXX (Central Office Code) is the 3-digit exchange prefix. Together, they represent the first 6 digits of a 10-digit phone number and determine geographic location, carrier assignment, and routing information.
Unlike raw NANPA assignment files, our database adds significant value through:
The database supports phone number validation, call routing systems, fraud detection, CRM enrichment, time zone scheduling, telecom analytics, and TCPA compliance applications.
Each edition is optimized for different technical use cases and implementation complexity:
| Feature Category | Standard (9 fields) | Deluxe (31 fields) |
|---|---|---|
| Core Identifiers | NPA, NXX, NPANXX, State, LATA | Same as Standard |
| ZIP Correlation | ZipCode, ZipCodeCount, ZipCodeFreq | Same as Standard |
| Geographic Data | None | Latitude, Longitude, WeightedLat, WeightedLon, City, County, CountyPop, FIPS, MSA_CBSA |
| Carrier & Routing | None | Company, OCN, RateCenter, CoverageAreaName |
| Service Classification | None | NXXUseType (W/L), Status (Active/Inactive), NXXIntroVersion |
| Time Intelligence | None | TimeZone, ObservesDST |
| Area Code Changes | None | NPANew, Overlay |
| File Sizes | CSV: 19 MB (3 MB compressed) MDB: 72 MB (7.5 MB compressed) |
CSV: 110 MB (9.5 MB compressed) MDB: 72 MB (21.5 MB compressed) |
| Database Footprint | ~40 MB uncompressed (indexed) | ~250 MB uncompressed (indexed) |
| Best Technical Use Cases |
• Basic NPA-NXX validation • ZIP-to-area-code lookups • Simple routing rules • Embedded applications (smaller footprint) |
• Advanced call routing (time zone, carrier) • Fraud detection (status, intro date) • CRM enrichment (city, county, demographics) • GIS integration (coordinates, FIPS) • TCPA compliance (wireless/landline) • Telecom analytics (comprehensive) |
Technical recommendation. Choose Standard for basic validation and routing where a minimal database footprint matters. Choose Deluxe when you need carrier attribution, geographic precision, time zone scheduling, or any analytics beyond a simple lookup. Most production systems end up wanting Deluxe.
You can upgrade from Standard to Deluxe at any time by paying the price difference. Your schema will need updating to accommodate the additional 22 fields, but the natural key — NPANXX plus ZipCode — is unchanged.
Our database is derived directly from NANPA (North American Numbering Plan Administration)-the official authority responsible for area code and central office code assignments. This is the same authoritative source used by all telecommunications carriers in North America.
Data Processing Pipeline:
Accuracy by Field Category:
| Data Category | Accuracy | Notes |
|---|---|---|
| NPA-NXX Assignments | 99.9%+ | Direct from NANPA; definitive source |
| Rate Centers, LATA | 99.9%+ | NANPA-assigned telecom geography |
| State, Geographic Location | 99%+ | Derived from rate center location |
| ZIP Code Correlations | 95-98% | Proprietary analysis; boundaries don't align perfectly |
| Carrier (Company, OCN) | 98%+ | Current at time of NANPA assignment; may change via mergers |
| Wireless/Landline Type | 85-90% | Original assignment type; individual numbers may have ported |
| Coordinates | 98%+ | Rate center location; ~1km precision typical |
Important Accuracy Considerations:
The Status field (Deluxe edition only) indicates whether an NPA-NXX combination is currently in service or reserved for future use. Understanding this distinction is critical for proper implementation.
Status Values Explained:
| Status | Definition | Technical Characteristics |
|---|---|---|
| Active | Currently assigned to a carrier and in service. Phone numbers are being issued from this block. |
Data Completeness: Full carrier, geographic, and routing data present Record Count: ~442,000 records (79%) Source: NANPA "utilized codes" files |
| Inactive | Reserved/provisioned by NANPA for future use and planned expansion but not yet assigned to a carrier. |
Data Completeness: Limited-typically only NPANXX, NPA, NXX, State, and Status populated. Some records include placeholder values: • RateCenter = XXXXXXXXXX• OCN = MULT• Company = MULTIPLE OCN LISTING• LATA = empty (100% of inactive records) • TimeZone may be present if state has single zone Record Count: ~116,000 records (21%) Source: NANPA "available codes" files |
Implementation Guidance:
-- Production routing and validation (only assigned blocks)
SELECT * FROM npanxx WHERE Status = 'Active';
-- Fraud detection (flag calls from unassigned blocks)
SELECT * FROM npanxx WHERE Status = 'Inactive';
-- Note: While not 100% definitive, calls from Inactive blocks
-- have significantly higher fraud probability
-- Planning & forecasting (complete dataset)
SELECT * FROM npanxx;
-- No filter; analyze both Active and Inactive for capacity planning
Common Use Cases for Inactive Records:
Status Transitions Over Time:
When NANPA assigns an Inactive block to a carrier, it becomes Active in our next quarterly update. The record is enriched with full carrier information (Company, OCN), complete geographic data, rate center assignment, and all routing details. The NPANXX key remains consistent, so you can track the transition by comparing quarterly releases.
NANPA enforces specific structural rules for valid area codes (NPA) and exchange prefixes (NXX). Understanding these constraints is essential for input validation and data integrity.
NPA (Area Code) Rules:
| Digit Position | Valid Range | Rule Explanation |
|---|---|---|
| First Digit (N) | 2-9 |
Never 0 or 1 (reserved for special services: 0=operator, 1=long distance prefix) |
| Second Digit (P) | 0-9 |
Any digit allowed. Historically was 0 or 1, but modern assignments use all digits |
| Third Digit (A) | 0-9 |
Any digit allowed |
NXX (Exchange Prefix) Rules:
| Digit Position | Valid Range | Rule Explanation |
|---|---|---|
| First Digit (N) | 2-9 |
Never 0 or 1 (same reasoning as NPA) |
| Second Digit (X) | 0-9 |
Any digit allowed |
| Third Digit (X) | 0-9 |
Any digit allowed |
Special Reserved Codes (N11):
Codes following the N11 pattern (where NPA or NXX has format X11) are reserved for special services and are not included in our database:
211 - Community information / non-emergency311 - Non-emergency services411 - Directory assistance511 - Traffic/travel information611 - Carrier customer service711 - Telecommunications relay service811 - Call before you dig911 - Emergency servicesImplementation Examples:
// JavaScript/TypeScript validation
function isValidNPA(npa: string): boolean {
if (npa.length !== 3) return false;
const first = parseInt(npa[0]);
return first >= 2 && first <= 9;
}
function isValidNXX(nxx: string): boolean {
if (nxx.length !== 3) return false;
const first = parseInt(nxx[0]);
return first >= 2 && first <= 9;
}
function isValidNPANXX(npanxx: string): boolean {
if (npanxx.length !== 6) return false;
return isValidNPA(npanxx.substring(0, 3)) &&
isValidNXX(npanxx.substring(3, 6));
}
// SQL constraint (PostgreSQL/MySQL)
ALTER TABLE npanxx ADD CONSTRAINT check_npa_valid
CHECK (SUBSTRING(NPA, 1, 1) BETWEEN '2' AND '9');
ALTER TABLE npanxx ADD CONSTRAINT check_nxx_valid
CHECK (SUBSTRING(NXX, 1, 1) BETWEEN '2' AND '9');
Our database uses empty strings ("") rather than NULL values for missing or unavailable data. This is a deliberate design choice with specific technical implications.
Why Empty Strings Instead of NULL?
Years ago, we worked with small businesses and individuals who understood "empty" but struggled with the null concept. Looking back, we probably should have stuck with NULL, but here we are. The good news: it's consistent across all fields, so once you understand the pattern, implementation is straightforward.
Field-by-Field Behavior:
| Field Category | Behavior When Unavailable | Example Fields |
|---|---|---|
| Always Populated | Never empty, never NULL | NPANXX, NPA, NXX, Status (Deluxe), State |
| Text Fields | Empty string "" |
City, County, Company, RateCenter, CoverageAreaName, MSA_CBSA |
| Numeric Fields | Empty string "" (stored as text) |
CountyPop, ZipCodeCount, ZipCodeFreq, TimeZone, Overlay |
| Coordinates (EXCEPTION) | Set to 0 (zero) |
Latitude, Longitude, WeightedLatitude, WeightedLongitude |
Critical Implementation Details:
XXXXXXXXXX, OCN=MULT, Company=MULTIPLE OCN LISTING. Treat these as semantically equivalent to empty strings.Query Pattern Examples:
-- SQL: Find records with missing city data
SELECT * FROM npanxx WHERE City = '' OR City IS NULL;
-- SQL: Find records with valid coordinates
SELECT * FROM npanxx WHERE Latitude != 0 AND Longitude != 0;
-- SQL: Count records missing carrier information
SELECT COUNT(*) FROM npanxx
WHERE Company = '' OR Company = 'MULTIPLE OCN LISTING';
-- Python: Check for empty fields
if record['City'] == '' or record['City'] is None:
# Handle missing city
-- Python: Validate coordinates (remember 0 = unknown)
if record['Latitude'] == 0 or record['Longitude'] == 0:
# Coordinates unavailable
-- JavaScript: Defensive checking
if (!field || field === '' || field === null) {
// Field is empty or missing
}
0 (zero), not empty string. This is the only exception to the empty-string rule. Always check for 0 values before using coordinates for mapping or distance calculations. A coordinate of (0, 0) represents "unknown location," not the Gulf of Guinea!
NULL during import if your application prefers NULL semantics. Use database functions like NULLIF(field, '') in MySQL/PostgreSQL or conditional logic in your import scripts. However, be aware that coordinate fields use 0, not empty strings, so handle them separately.
Latitude and Longitude fields use 0 (zero) to indicate unknown or unavailable coordinates, which is an exception to our general empty-string convention. This design choice has important technical implications.
Why Zero Instead of Empty String?
Geographic coordinate fields are typically stored as numeric types (DECIMAL or FLOAT) rather than text. While text fields can store empty strings, numeric fields require an actual numeric value. We chose 0 as a sentinel value because:
Geographic Reality of (0, 0):
The coordinate (0°, 0°) represents a location in the Atlantic Ocean off the coast of West Africa (Gulf of Guinea), approximately 380 miles south of Ghana. Since no North American telephone infrastructure exists at this location, 0 serves as an unambiguous indicator of missing data.
Fields Affected:
Latitude (Deluxe only)Longitude (Deluxe only)WeightedLatitude (Deluxe only)WeightedLongitude (Deluxe only)Implementation Patterns:
-- SQL: Filter to records with valid coordinates
SELECT * FROM npanxx
WHERE Latitude != 0 AND Longitude != 0;
-- SQL: Count records missing coordinates
SELECT COUNT(*) FROM npanxx
WHERE Latitude = 0 OR Longitude = 0;
-- SQL: Safe distance calculation (skip invalid coordinates)
SELECT
NPANXX,
CASE
WHEN Latitude != 0 AND Longitude != 0
THEN SQRT(POW(Latitude - @targetLat, 2) + POW(Longitude - @targetLon, 2))
ELSE NULL
END AS rough_distance
FROM npanxx;
-- Python: Validate before mapping
def has_valid_coordinates(record):
return (record['Latitude'] != 0 and
record['Longitude'] != 0 and
record['Latitude'] is not None and
record['Longitude'] is not None)
if has_valid_coordinates(record):
plot_on_map(record['Latitude'], record['Longitude'])
else:
print(f"No coordinates for {record['NPANXX']}")
-- JavaScript: Safe coordinate access
function getCoordinates(record) {
if (record.Latitude === 0 || record.Longitude === 0) {
return null; // Invalid/unknown
}
return {
lat: parseFloat(record.Latitude),
lng: parseFloat(record.Longitude)
};
}
0. Failing to validate will cause mapping applications to plot points in the Atlantic Ocean, distance calculations to be wildly inaccurate, and spatial queries to return incorrect results. Always filter WHERE Latitude != 0 AND Longitude != 0 before any geographic operation.
When Are Coordinates Zero?
0 since geographic assignment hasn't occurredUPDATE npanxx SET Latitude = NULL WHERE Latitude = 0. This allows database NULL handling but requires schema modification. Most customers keep the 0 convention and validate in application logic.
The ZipCodeFreq field uses a counter-intuitive ranking system where negative numbers indicate correlation strength. Understanding this system is critical for accurate ZIP code attribution.
ZipCodeFreq Value Meanings:
| Value | Ranking | Interpretation & Usage |
|---|---|---|
-1 |
Primary |
Most frequent ZIP code associated with this NPA-NXX. Use for: Primary geographic attribution, mailing address defaults, single-ZIP lookups Confidence: Highest-typically represents 40-80% of lines in the block Example: NPA-NXX 212-555 → ZipCode 10001 (ZipCodeFreq=-1, ZipCodeCount=5423) |
-2 |
Secondary |
Second most frequent ZIP code for this NPA-NXX. Use for: Comprehensive coverage analysis, service area mapping, fallback lookups Confidence: Moderate-typically 10-30% of lines in the block Example: NPA-NXX 212-555 → ZipCode 10002 (ZipCodeFreq=-2, ZipCodeCount=1876) |
0 |
Unknown |
Frequency data unavailable or statistically insignificant (<2% of lines). Use for: When no better data exists; treat with caution Confidence: Low-use only as last resort Note: Records with ZipCodeFreq=0 may still have a ZipCode populated, but the correlation is weak |
Why Negative Numbers?
The negative ranking system (-1, -2) was chosen to sort naturally in descending order. When you sort by ZipCodeFreq ascending, the most important records (-1) appear first, followed by secondary (-2), then unknown (0). This makes database queries more intuitive.
Implementation Patterns:
-- Get primary ZIP code only (most common use case)
SELECT ZipCode, ZipCodeCount
FROM npanxx
WHERE NPANXX = '212555' AND ZipCodeFreq = -1;
-- Get primary and secondary ZIPs (comprehensive coverage)
SELECT ZipCode, ZipCodeFreq, ZipCodeCount
FROM npanxx
WHERE NPANXX = '212555'
AND ZipCodeFreq IN (-1, -2)
ORDER BY ZipCodeFreq ASC; -- Returns -1 first, then -2
-- Find all NPA-NXX blocks serving a specific ZIP
SELECT NPANXX, NPA, NXX, ZipCodeFreq, ZipCodeCount
FROM npanxx
WHERE ZipCode = '10001' AND ZipCodeFreq = -1
ORDER BY ZipCodeCount DESC; -- Highest population first
-- Validate correlation strength
SELECT
NPANXX,
ZipCode,
ZipCodeFreq,
ZipCodeCount,
CASE
WHEN ZipCodeFreq = -1 THEN 'High Confidence'
WHEN ZipCodeFreq = -2 THEN 'Moderate Confidence'
ELSE 'Low Confidence'
END AS ConfidenceLevel
FROM npanxx
WHERE NPANXX = '212555';
Real-World Example:
Consider NPA-NXX 212-555 (New York):
| ZipCode | ZipCodeFreq | ZipCodeCount | Interpretation |
|---|---|---|---|
| 10001 | -1 | 5,423 | Primary: ~65% of phone lines in block |
| 10002 | -2 | 1,876 | Secondary: ~22% of phone lines |
| 10003 | 0 | 987 | Low confidence: ~13% (statistically insignificant, excluded from ranking) |
ZipCodeFreq = -1 to get the primary ZIP correlation. This provides optimal accuracy while keeping dataset size manageable. Include ZipCodeFreq = -2 only when comprehensive geographic coverage is required (e.g., service territory mapping, demographic analysis across all ZIP codes served).
Note: Since a single NPA-NXX can map to multiple ZIP codes (one record per correlation), the NPANXX field is not unique in the database. Always include ZipCodeFreq in your queries to control which correlation level you're retrieving.
The NXXUseType field classifies NPA-NXX blocks as Wireless (W) or Landline (L) based on original NANPA carrier assignment. Understanding the reliability and limitations of this classification is critical for compliance and business logic.
Classification Accuracy:
At the block level (NPA-NXX), the classification is 85-90% accurate for identifying the original carrier type. However, at the individual phone number level, accuracy can be significantly lower due to number portability.
Why Accuracy Varies:
Appropriate Use Cases:
| Use Case | Reliability | Implementation Notes |
|---|---|---|
| Statistical Analysis | High | Excellent for aggregate analysis, market research, trend identification. Errors average out across large datasets. |
| Initial Classification | High | Good first-pass filter before deeper validation. Fast and cost-effective for bulk processing. |
| Risk Scoring | Moderate | Useful as one signal among many. Wireless numbers have different fraud profiles than landlines but not definitive. |
| TCPA Compliance | Low | Not sufficient alone. FCC regulations require accurate wireless identification. Supplement with real-time lookup services for legal compliance. |
| Marketing Segmentation | Moderate | Acceptable for targeting strategies but understand ~10-15% may be misclassified. |
Implementation Example:
-- Two-stage approach: Filter first, verify critical ones
-- Stage 1: Bulk filter using our database (fast, free)
SELECT * FROM call_queue cq
JOIN npanxx n ON SUBSTRING(cq.phone, 1, 6) = n.NPANXX
WHERE n.NXXUseType = 'W' AND n.Status = 'Active'
AND n.ZipCodeFreq = -1;
-- Result: Likely wireless numbers (85-90% accuracy)
-- Stage 2: For TCPA-critical calls, verify via real-time API
-- (pseudocode)
foreach (likelyWireless in filteredList) {
if (requiresTCPACompliance) {
definitive = callWirelessVerificationAPI(likelyWireless.phone);
if (definitive.isWireless) {
// Ensure explicit consent before calling
}
}
}
Field Values:
W - Wireless (mobile/cellular)L - Landline (traditional wireline)"" - Empty string (classification unknown or not applicable)For general information about wireless/landline classification, see the overview FAQ. This technical FAQ provides implementation-specific guidance for developers.
No-NPANXX is not unique in the database and therefore cannot serve as a primary key. This is a critical design consideration that trips up many first-time implementers.
Why NPANXX Is Not Unique:
A single NPA-NXX combination can serve multiple ZIP codes. Since telephone exchange boundaries and ZIP code boundaries don't align perfectly, one NPA-NXX block may have phone numbers distributed across 2-5 different ZIP codes. Our database includes one record per NPA-NXX/ZIP correlation, resulting in duplicate NPANXX values.
Example:
| NPANXX | ZipCode | ZipCodeFreq | ZipCodeCount |
|---|---|---|---|
| 212555 | 10001 | -1 (Primary) | 5,423 |
| 212555 | 10002 | -2 (Secondary) | 1,876 |
| 212555 | 10003 | 0 (Unknown) | 987 |
Recommended Primary Key Strategies:
Strategy 1: Auto-Increment Surrogate Key (Most Common)
CREATE TABLE npanxx (
id INT AUTO_INCREMENT PRIMARY KEY,
NPANXX CHAR(6) NOT NULL,
NPA CHAR(3) NOT NULL,
NXX CHAR(3) NOT NULL,
ZipCode VARCHAR(10),
ZipCodeFreq INT,
State VARCHAR(2),
-- ... additional fields
INDEX idx_npanxx (NPANXX),
INDEX idx_npanxx_zip (NPANXX, ZipCode),
INDEX idx_zip (ZipCode)
);
Pros: Simple, guaranteed unique, works with ORMs. Cons: ID values change across quarterly updates (not stable for external references).
Strategy 2: Composite Primary Key
CREATE TABLE npanxx (
NPANXX CHAR(6) NOT NULL,
ZipCode VARCHAR(10) NOT NULL,
ZipCodeFreq INT,
State VARCHAR(2),
-- ... additional fields
PRIMARY KEY (NPANXX, ZipCode),
INDEX idx_npanxx (NPANXX),
INDEX idx_zip (ZipCode)
);
Pros: Natural key, stable across updates, enforces uniqueness constraint. Cons: More complex joins, some ORMs struggle with composite keys.
Strategy 3: Single-ZIP Subset (If You Don't Need Multiple ZIPs)
-- Import only primary ZIP correlations
CREATE TABLE npanxx (
NPANXX CHAR(6) PRIMARY KEY,
ZipCode VARCHAR(10),
ZipCodeFreq INT DEFAULT -1,
State VARCHAR(2),
-- ... additional fields
);
-- During import, filter to primary ZIP only
LOAD DATA LOCAL INFILE 'npanxx.csv'
INTO TABLE npanxx
WHERE ZipCodeFreq = -1;
Pros: NPANXX becomes unique, simplifies schema. Cons: Loses secondary ZIP correlations, less comprehensive coverage.
Strategy 4: GUID/UUID Surrogate Key
CREATE TABLE npanxx (
uuid CHAR(36) PRIMARY KEY DEFAULT (UUID()),
NPANXX CHAR(6) NOT NULL,
ZipCode VARCHAR(10),
-- ... additional fields
INDEX idx_npanxx (NPANXX),
INDEX idx_npanxx_zip (NPANXX, ZipCode)
);
Pros: Globally unique, stable across systems, works for distributed databases. Cons: Larger storage footprint, slower joins than INT keys.
Common Mistake to Avoid:
-- DON'T DO THIS - Will fail on duplicate NPANXX
CREATE TABLE npanxx (
NPANXX CHAR(6) PRIMARY KEY, -- WRONG! Not unique!
ZipCode VARCHAR(10),
-- ...
);
-- Import will fail with duplicate key error:
-- ERROR 1062: Duplicate entry '212555' for key 'PRIMARY'
Update Strategy: If using auto-increment IDs, remember they're not stable across quarterly updates. Don't store ID references externally. Use (NPANXX, ZipCode) composite as your logical key for cross-quarter tracking.
Proper indexing is critical for query performance. With ~558,000 records, well-designed indexes enable millisecond lookups; poor indexing leads to full table scans and multi-second response times.
Essential Indexes (Required for All Implementations):
-- Index 1: NPANXX lookup (most common query pattern)
CREATE INDEX idx_npanxx ON npanxx(NPANXX);
-- Index 2: NPA (area code) lookup
CREATE INDEX idx_npa ON npanxx(NPA);
-- Index 3: ZIP code reverse lookup
CREATE INDEX idx_zipcode ON npanxx(ZipCode);
-- Index 4: State filtering (for geographic queries)
CREATE INDEX idx_state ON npanxx(State);
Recommended Composite Indexes (Query Pattern Optimization):
-- Index 5: NPANXX + ZipCodeFreq (primary ZIP lookup)
CREATE INDEX idx_npanxx_freq ON npanxx(NPANXX, ZipCodeFreq);
-- Optimizes: WHERE NPANXX = ? AND ZipCodeFreq = -1
-- Index 6: NPANXX + ZipCode (composite key stability)
CREATE INDEX idx_npanxx_zip ON npanxx(NPANXX, ZipCode);
-- Optimizes: WHERE NPANXX = ? AND ZipCode = ?
-- Index 7: State + Status (active codes by state)
CREATE INDEX idx_state_status ON npanxx(State, Status);
-- Optimizes: WHERE State = ? AND Status = 'Active'
-- Index 8: Status + NPANXX (active/inactive filtering)
CREATE INDEX idx_status_npanxx ON npanxx(Status, NPANXX);
-- Optimizes: WHERE Status = 'Active' ORDER BY NPANXX
Advanced Indexes (Deluxe Edition, Specialized Use Cases):
-- Index 9: Carrier lookup (OCN)
CREATE INDEX idx_ocn ON npanxx(OCN);
-- Optimizes: WHERE OCN = ? (find all blocks by carrier)
-- Index 10: Time zone scheduling
CREATE INDEX idx_timezone ON npanxx(TimeZone, ObservesDST);
-- Optimizes: WHERE TimeZone = ? AND ObservesDST = 'Y'
-- Index 11: Wireless/Landline filtering
CREATE INDEX idx_nxxusetype ON npanxx(NXXUseType, Status);
-- Optimizes: WHERE NXXUseType = 'W' AND Status = 'Active'
-- Index 12: Geographic queries (coordinates)
-- Note: Only index if doing spatial queries
CREATE INDEX idx_coordinates ON npanxx(Latitude, Longitude);
-- Optimizes: Bounding box queries, proximity searches
Query Performance Examples:
| Query Type | Without Index | With Index | Speed Improvement |
|---|---|---|---|
| Single NPANXX lookup | ~120ms (full scan) | <1ms | 120x faster |
| ZIP code reverse lookup | ~150ms (full scan) | 1-2ms | 75-150x faster |
| State + Status filter | ~80ms (partial scan) | <1ms | 80x faster |
| Batch processing (1M lookups) | ~33 hours | ~17 minutes | 117x faster |
Index Maintenance Considerations:
Optimal Bulk Import Strategy:
-- Step 1: Drop indexes before quarterly import
DROP INDEX idx_npanxx ON npanxx;
DROP INDEX idx_npa ON npanxx;
-- ... drop all indexes
-- Step 2: Truncate and import data (10x faster without indexes)
TRUNCATE TABLE npanxx;
LOAD DATA LOCAL INFILE 'npanxx.csv' INTO TABLE npanxx ...;
-- Step 3: Rebuild indexes (one-time cost)
CREATE INDEX idx_npanxx ON npanxx(NPANXX);
CREATE INDEX idx_npa ON npanxx(NPA);
-- ... recreate all indexes
-- Step 4: Analyze table for query optimization
ANALYZE TABLE npanxx;
Verify Index Usage:
-- Check if your query uses indexes (MySQL)
EXPLAIN SELECT * FROM npanxx WHERE NPANXX = '212555';
-- Look for "Using index" in the Extra column
-- PostgreSQL equivalent
EXPLAIN ANALYZE SELECT * FROM npanxx WHERE NPANXX = '212555';
-- Look for "Index Scan" instead of "Seq Scan"
Don't Over-Index: Only create indexes for fields you actually query. Unused indexes waste storage and slow down imports. Monitor query patterns for 2-4 weeks after launch, then add targeted indexes for slow queries. Start with the essential 4-8 indexes above and expand as needed.
Proper data type selection ensures data integrity, optimal storage, and query performance. Here are field-by-field recommendations with rationale.
Standard Edition Fields:
| Field Name | Recommended Type | Rationale |
|---|---|---|
| NPA | CHAR(3) |
Fixed 3 characters, preserves leading zeros (e.g., "007"). Don't use INT-loses leading zeros. |
| NXX | CHAR(3) |
Fixed 3 characters, preserves leading zeros. Same reasoning as NPA. |
| NPANXX | CHAR(6) |
Fixed 6 characters (NPA + NXX concatenated). Primary lookup field. |
| State | VARCHAR(2) |
2-character codes (e.g., "NY", "CA"). VARCHAR allows rare empty cases. |
| ZipCode | VARCHAR(10) |
Accommodates 5-digit US ZIPs and Canadian postal codes (e.g., "M5H 2N2"). VARCHAR for variable length. |
| ZipCodeCount | INT or INTEGER |
Population/line count estimate. Whole numbers only, no decimals needed. |
| ZipCodeFreq | INT or TINYINT |
Values: -1, -2, 0. TINYINT sufficient but INT more common for simplicity. |
| LATA | VARCHAR(10) |
Typically 3-5 characters but variable length. May be empty for inactive records. |
Deluxe Edition Additional Fields:
| Field Name | Recommended Type | Rationale |
|---|---|---|
| Latitude | DECIMAL(10,6) |
6 decimal places = ~11cm precision. Sufficient for rate center accuracy. Note: Use 0 for unknown, not NULL. |
| Longitude | DECIMAL(10,6) |
Same as Latitude. Consistent precision for distance calculations. |
| WeightedLatitude | DECIMAL(10,6) |
Same precision as Latitude for consistency. |
| WeightedLongitude | DECIMAL(10,6) |
Same precision as Longitude for consistency. |
| City | VARCHAR(100) |
Most cities <30 chars, but some long names exist. 100 is safe buffer. |
| County | VARCHAR(100) |
County names vary widely. 100 accommodates longest US county names. |
| CountyPop | INT or INTEGER |
US Census population counts. INT handles up to 2.1 billion (sufficient for all US counties). |
| FIPS | VARCHAR(10) |
5-digit codes (e.g., "36061"). VARCHAR preserves leading zeros and allows empty values. |
| MSA_CBSA | VARCHAR(255) |
Metro area names can be long (e.g., "New York-Newark-Jersey City, NY-NJ-PA"). 255 is standard text field. |
| Company | VARCHAR(255) |
Carrier names vary. Some are short ("AT&T"), others long with subsidiaries. |
| OCN | VARCHAR(10) |
4-character codes typically, but VARCHAR allows empty and placeholder values ("MULT"). |
| RateCenter | VARCHAR(100) |
Rate center names vary in length. Allow for long names and placeholder "XXXXXXXXXX". |
| NXXUseType | VARCHAR(2) |
Values: "W", "L", or empty string. CHAR(2) also acceptable. |
| Status | VARCHAR(20) |
Values: "Active", "Inactive". VARCHAR allows future expansion if needed. |
| NXXIntroVersion | VARCHAR(20) or DATE |
Format: "M/D/YYYY" (e.g., "1/25/1998"). VARCHAR stores as-is; DATE requires parsing during import. |
| NPANew | VARCHAR(10) |
3-character area codes, but may be empty. VARCHAR allows flexibility. |
| Overlay | INT or TINYINT |
Boolean-style: 0 or 1. TINYINT more storage-efficient but INT more common. |
| TimeZone | INT or TINYINT |
UTC offset hours (-11 to -4 for North America). TINYINT sufficient. |
| ObservesDST | VARCHAR(2) or CHAR(1) |
Values: "Y", "N". CHAR(1) more efficient if you only need single character. |
| CoverageAreaName | VARCHAR(255) |
Marketing/service region names. Variable length, may be empty. |
Complete MySQL Schema Example:
CREATE TABLE npanxx (
id INT AUTO_INCREMENT PRIMARY KEY,
-- Core identifiers (Standard Edition)
NPA CHAR(3) NOT NULL,
NXX CHAR(3) NOT NULL,
NPANXX CHAR(6) NOT NULL,
State VARCHAR(2),
LATA VARCHAR(10),
-- ZIP correlation (Standard Edition)
ZipCode VARCHAR(10),
ZipCodeCount INT,
ZipCodeFreq INT,
-- Geographic data (Deluxe Edition)
Latitude DECIMAL(10,6),
Longitude DECIMAL(10,6),
WeightedLatitude DECIMAL(10,6),
WeightedLongitude DECIMAL(10,6),
City VARCHAR(100),
County VARCHAR(100),
CountyPop INT,
FIPS VARCHAR(10),
MSA_CBSA VARCHAR(255),
-- Carrier & routing (Deluxe Edition)
Company VARCHAR(255),
OCN VARCHAR(10),
RateCenter VARCHAR(100),
CoverageAreaName VARCHAR(255),
-- Service classification (Deluxe Edition)
NXXUseType VARCHAR(2),
Status VARCHAR(20),
NXXIntroVersion VARCHAR(20),
-- Area code changes (Deluxe Edition)
NPANew VARCHAR(10),
Overlay INT,
-- Time intelligence (Deluxe Edition)
TimeZone INT,
ObservesDST VARCHAR(2),
-- Indexes
INDEX idx_npanxx (NPANXX),
INDEX idx_npa (NPA),
INDEX idx_zipcode (ZipCode),
INDEX idx_state (State)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
utf8mb4 charset with utf8mb4_unicode_ci collation (MySQL) or UTF8 encoding (PostgreSQL). This ensures proper handling of special characters in carrier names and geographic locations. Our CSV files are UTF-8 encoded, so database charset must match.
All data files (CSV, TAB, and Access MDB) use UTF-8 character encoding to ensure proper handling of special characters, international names, and symbols. Understanding encoding is critical to avoid data corruption during import.
Encoding Specifications:
Why UTF-8 Matters:
While most data contains standard ASCII characters, certain fields may include special characters:
Import Configuration by Platform:
MySQL:
-- Set database character set
CREATE DATABASE npanxx_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Import with explicit encoding
LOAD DATA LOCAL INFILE 'npanxx.csv'
INTO TABLE npanxx
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 ROWS;
PostgreSQL:
-- Set database encoding
CREATE DATABASE npanxx_db
ENCODING 'UTF8'
LC_COLLATE='en_US.UTF-8'
LC_CTYPE='en_US.UTF-8';
-- Import automatically uses UTF-8 if database is UTF-8
COPY npanxx FROM '/path/to/npanxx.csv'
DELIMITER ',' CSV HEADER;
SQL Server:
-- Use nvarchar for Unicode support
CREATE TABLE npanxx (
...
Company NVARCHAR(255), -- Note: NVARCHAR not VARCHAR
City NVARCHAR(100),
...
);
-- Import with UTF-8 code page (65001)
BULK INSERT npanxx
FROM 'C:\path\to\npanxx.csv'
WITH (
CODEPAGE = '65001', -- UTF-8
FIELDTERMINATOR = ',',
ROWTERMINATOR = '\n',
FIRSTROW = 2
);
Verification After Import:
-- Check for encoding issues
SELECT Company FROM npanxx WHERE Company LIKE '%&%';
-- Should return clean "AT&T", not "AT&T"
SELECT Company FROM npanxx WHERE Company LIKE '%?%';
-- Question marks often indicate failed character conversion
-- Verify database encoding (MySQL)
SHOW VARIABLES LIKE 'character_set%';
-- Verify database encoding (PostgreSQL)
SHOW SERVER_ENCODING;
Note: Our CSV files are UTF-8 without BOM (Byte Order Mark). Some text editors add BOM automatically when saving-avoid this as it can cause the first field name to be misread during import (e.g., "?NPANXX" with invisible BOM character).
Detailed platform-specific import instructions with complete examples. Choose your database platform below for copy-paste-ready code.
MySQL / MariaDB Import:
-- Step 1: Create database
CREATE DATABASE npanxx_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE npanxx_db;
-- Step 2: Create table (see Question 12 for complete schema)
CREATE TABLE npanxx (
id INT AUTO_INCREMENT PRIMARY KEY,
NPANXX CHAR(6) NOT NULL,
NPA CHAR(3) NOT NULL,
NXX CHAR(3) NOT NULL,
State VARCHAR(2),
ZipCode VARCHAR(10),
ZipCodeCount INT,
ZipCodeFreq INT,
LATA VARCHAR(10),
-- Add remaining fields based on edition
INDEX idx_npanxx (NPANXX),
INDEX idx_npa (NPA),
INDEX idx_zipcode (ZipCode),
INDEX idx_state (State)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Step 3: Import CSV
LOAD DATA LOCAL INFILE '/path/to/npanxx.csv'
INTO TABLE npanxx
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 ROWS
(NPA, NXX, @ignore, ZipCode, ZipCodeCount, ZipCodeFreq, @ignore, NPANXX, State, LATA)
SET id = NULL;
-- Step 4: Verify import
SELECT COUNT(*) FROM npanxx;
SELECT * FROM npanxx LIMIT 10;
-- Step 5: Optimize table
ANALYZE TABLE npanxx;
Troubleshooting MySQL Import:
SET GLOBAL local_infile = 1;PostgreSQL Import:
-- Step 1: Create database
CREATE DATABASE npanxx_db
ENCODING 'UTF8'
LC_COLLATE='en_US.UTF-8'
LC_CTYPE='en_US.UTF-8';
\c npanxx_db
-- Step 2: Create table
CREATE TABLE npanxx (
id SERIAL PRIMARY KEY,
NPANXX CHAR(6) NOT NULL,
NPA CHAR(3) NOT NULL,
NXX CHAR(3) NOT NULL,
State VARCHAR(2),
ZipCode VARCHAR(10),
ZipCodeCount INTEGER,
ZipCodeFreq INTEGER,
LATA VARCHAR(10)
-- Add remaining fields based on edition
);
-- Step 3: Create indexes
CREATE INDEX idx_npanxx ON npanxx(NPANXX);
CREATE INDEX idx_npa ON npanxx(NPA);
CREATE INDEX idx_zipcode ON npanxx(ZipCode);
CREATE INDEX idx_state ON npanxx(State);
-- Step 4: Import CSV
COPY npanxx(NPA, NXX, ZipCode, ZipCodeCount, ZipCodeFreq, NPANXX, State, LATA)
FROM '/absolute/path/to/npanxx.csv'
DELIMITER ','
CSV HEADER;
-- Step 5: Verify import
SELECT COUNT(*) FROM npanxx;
SELECT * FROM npanxx LIMIT 10;
-- Step 6: Update table statistics
ANALYZE npanxx;
Troubleshooting PostgreSQL Import:
\COPY command (runs as your user)\COPY npanxx FROM 'npanxx.csv' DELIMITER ',' CSV HEADER;SQL Server Import:
-- Step 1: Create database
CREATE DATABASE npanxx_db;
GO
USE npanxx_db;
GO
-- Step 2: Create table
CREATE TABLE npanxx (
id INT IDENTITY(1,1) PRIMARY KEY,
NPANXX CHAR(6) NOT NULL,
NPA CHAR(3) NOT NULL,
NXX CHAR(3) NOT NULL,
State VARCHAR(2),
ZipCode VARCHAR(10),
ZipCodeCount INT,
ZipCodeFreq INT,
LATA VARCHAR(10)
-- Add remaining fields (use NVARCHAR for Unicode support)
);
-- Step 3: Create indexes
CREATE INDEX idx_npanxx ON npanxx(NPANXX);
CREATE INDEX idx_npa ON npanxx(NPA);
CREATE INDEX idx_zipcode ON npanxx(ZipCode);
CREATE INDEX idx_state ON npanxx(State);
GO
-- Step 4: Import CSV
BULK INSERT npanxx
FROM 'C:\path\to\npanxx.csv'
WITH (
CODEPAGE = '65001', -- UTF-8
DATAFILETYPE = 'char',
FIELDTERMINATOR = ',',
ROWTERMINATOR = '\n',
FIRSTROW = 2, -- Skip header
TABLOCK
);
-- Step 5: Verify import
SELECT COUNT(*) FROM npanxx;
SELECT TOP 10 * FROM npanxx;
-- Step 6: Update statistics
UPDATE STATISTICS npanxx;
Troubleshooting SQL Server Import:
\\server\share\npanxx.csvAlternative: Import via Programming Language:
// Python with pandas (works for all databases)
import pandas as pd
from sqlalchemy import create_engine
# Read CSV
df = pd.read_csv('npanxx.csv', encoding='utf-8')
# Create database connection
engine = create_engine('mysql+pymysql://user:pass@localhost/npanxx_db')
# Import to database
df.to_sql('npanxx', con=engine, if_exists='replace', index=False)
print(f"Imported {len(df)} records")
For more detailed schema definitions including all Deluxe edition fields, see Question 12: Data Type Recommendations.
When processing large volumes of phone numbers (1M+ records), query optimization and batching strategies are critical. Poor implementation can take hours; optimized approaches complete in minutes.
Performance Comparison:
| Approach | 1M Lookups | 10M Lookups | Scalability |
|---|---|---|---|
| Row-by-row queries (naive) | ~8 hours | ~80 hours | Terrible |
| Batched queries (1000/batch) | ~45 minutes | ~7.5 hours | Moderate |
| JOIN with temp table | ~3 minutes | ~30 minutes | Excellent |
| Load to memory (in-app) | ~2 minutes | ~20 minutes | Best |
Strategy 1: JOIN with Temporary Table (Recommended for SQL-based processing)
-- Step 1: Create temporary table with phone numbers to lookup
CREATE TEMPORARY TABLE phone_lookup (
id INT AUTO_INCREMENT PRIMARY KEY,
phone_number VARCHAR(15),
npanxx_key CHAR(6),
INDEX idx_npanxx (npanxx_key)
);
-- Step 2: Bulk insert your phone numbers
LOAD DATA LOCAL INFILE 'phones_to_lookup.csv'
INTO TABLE phone_lookup
(phone_number)
SET npanxx_key = LEFT(phone_number, 6);
-- Step 3: Single JOIN query (processes all records at once)
SELECT
pl.phone_number,
n.NPANXX,
n.State,
n.City,
n.ZipCode,
n.TimeZone,
n.NXXUseType,
n.Company
FROM phone_lookup pl
LEFT JOIN npanxx n ON pl.npanxx_key = n.NPANXX
WHERE n.Status = 'Active' AND n.ZipCodeFreq = -1;
-- Result: 1M records processed in ~3 minutes with proper indexes
Strategy 2: In-Memory Lookup (Best for application-level processing)
// Python example with pandas (works similarly in other languages)
import pandas as pd
# Step 1: Load entire NPANXX database into memory (~250MB for Deluxe)
npanxx_db = pd.read_csv('npanxx.csv', dtype={'NPANXX': str})
# Step 2: Filter to primary ZIP correlations (optional, reduces memory)
npanxx_db = npanxx_db[npanxx_db['ZipCodeFreq'] == -1]
# Step 3: Create lookup dictionary (O(1) lookup time)
npanxx_dict = npanxx_db.set_index('NPANXX').to_dict('index')
# Step 4: Process phone numbers
def lookup_phone(phone):
npanxx_key = phone[:6]
return npanxx_dict.get(npanxx_key, None)
# Step 5: Batch process millions of records
phones = pd.read_csv('phones_to_process.csv')
phones['npanxx_data'] = phones['phone'].apply(lookup_phone)
# Result: 1M records in ~2 minutes, scales linearly
Strategy 3: Indexed Batch Queries (When temp tables aren't available)
-- Extract unique NPA-NXX combinations first (deduplication)
SELECT DISTINCT LEFT(phone_number, 6) AS npanxx_key
FROM customer_phones
INTO OUTFILE '/tmp/unique_npanxx.csv';
-- Batch lookup with IN clause (max 1000 items per batch)
SELECT NPANXX, State, City, ZipCode, TimeZone, NXXUseType
FROM npanxx
WHERE NPANXX IN (
'212555', '213555', '214555', ... -- up to 1000 values
)
AND Status = 'Active' AND ZipCodeFreq = -1;
-- Repeat for each batch of 1000 unique NPA-NXX codes
Strategy 4: Database Views for Repeated Queries
-- Create optimized view (primary ZIPs, active only)
CREATE VIEW npanxx_primary AS
SELECT
NPANXX, NPA, NXX, State, ZipCode, City, County,
Latitude, Longitude, TimeZone, ObservesDST,
NXXUseType, Company, OCN
FROM npanxx
WHERE Status = 'Active' AND ZipCodeFreq = -1;
-- Use view for all lookups (pre-filtered, faster)
SELECT phone_number, n.*
FROM customer_phones cp
JOIN npanxx_primary n ON LEFT(cp.phone_number, 6) = n.NPANXX;
Optimization Checklist:
Real-World Example: CRM Enrichment
-- Scenario: Enrich 5M customer records with time zone data
-- Step 1: Add columns to customer table
ALTER TABLE customers
ADD COLUMN npanxx CHAR(6),
ADD COLUMN timezone INT,
ADD COLUMN observes_dst VARCHAR(2);
-- Step 2: Extract NPA-NXX from phone numbers
UPDATE customers
SET npanxx = LEFT(phone, 6)
WHERE phone IS NOT NULL AND LENGTH(phone) >= 6;
-- Step 3: Single UPDATE with JOIN (completes in minutes)
UPDATE customers c
JOIN npanxx n ON c.npanxx = n.NPANXX
SET
c.timezone = n.TimeZone,
c.observes_dst = n.ObservesDST
WHERE n.Status = 'Active' AND n.ZipCodeFreq = -1;
-- Result: 5M customers enriched in ~8 minutes
LIKE or substring functions in WHERE clauses without pre-extracting values. This prevents index usage:
WHERE LEFT(phone, 6) = n.NPANXX (can't use index)
WHERE npanxx_key = n.NPANXX (uses index)
Quarterly updates synchronize with NANPA releases and include new assignments, status changes, and data corrections. Choosing the right update strategy depends on your uptime requirements and change tracking needs.
Update Approach Comparison:
| Strategy | Downtime | Complexity | Change Tracking | Best For |
|---|---|---|---|---|
| Full Replacement | 5-15 minutes | Simple | None | Most applications |
| Table Swap (Blue-Green) | <1 second | Moderate | None | High-availability systems |
| Incremental Merge | None | Complex | Yes | Change auditing required |
| Dual Version | None | Moderate | Manual | Testing & validation |
Strategy 1: Full Replacement (Recommended for Most Use Cases)
-- Step 1: Drop indexes (speeds up import 10x)
DROP INDEX idx_npanxx ON npanxx;
DROP INDEX idx_npa ON npanxx;
DROP INDEX idx_zipcode ON npanxx;
DROP INDEX idx_state ON npanxx;
-- Step 2: Clear existing data
TRUNCATE TABLE npanxx;
-- Step 3: Import new quarterly data
LOAD DATA LOCAL INFILE '/path/to/npanxx_Q1_2025.csv'
INTO TABLE npanxx
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 ROWS;
-- Step 4: Rebuild indexes
CREATE INDEX idx_npanxx ON npanxx(NPANXX);
CREATE INDEX idx_npa ON npanxx(NPA);
CREATE INDEX idx_zipcode ON npanxx(ZipCode);
CREATE INDEX idx_state ON npanxx(State);
-- Step 5: Update statistics
ANALYZE TABLE npanxx;
-- Total downtime: 5-15 minutes depending on server
Strategy 2: Table Swap / Blue-Green Deployment (Zero Downtime)
See Question 17: Zero-Downtime Updates for detailed implementation.
Strategy 3: Incremental Merge (For Change Tracking)
-- Step 1: Create staging table with new data
CREATE TABLE npanxx_staging LIKE npanxx;
LOAD DATA LOCAL INFILE '/path/to/npanxx_Q1_2025.csv'
INTO TABLE npanxx_staging ...;
-- Step 2: Identify changes (new, updated, deleted)
-- New records
INSERT INTO npanxx_changes (change_type, npanxx, zipcode, timestamp)
SELECT 'NEW', s.NPANXX, s.ZipCode, NOW()
FROM npanxx_staging s
LEFT JOIN npanxx p ON s.NPANXX = p.NPANXX AND s.ZipCode = p.ZipCode
WHERE p.NPANXX IS NULL;
-- Updated records (compare critical fields)
INSERT INTO npanxx_changes (change_type, npanxx, zipcode, timestamp)
SELECT 'UPDATED', s.NPANXX, s.ZipCode, NOW()
FROM npanxx_staging s
JOIN npanxx p ON s.NPANXX = p.NPANXX AND s.ZipCode = p.ZipCode
WHERE s.Status != p.Status
OR s.Company != p.Company
OR s.City != p.City;
-- Deleted records
INSERT INTO npanxx_changes (change_type, npanxx, zipcode, timestamp)
SELECT 'DELETED', p.NPANXX, p.ZipCode, NOW()
FROM npanxx p
LEFT JOIN npanxx_staging s ON p.NPANXX = s.NPANXX AND p.ZipCode = s.ZipCode
WHERE s.NPANXX IS NULL;
-- Step 3: Apply changes
DELETE FROM npanxx;
INSERT INTO npanxx SELECT * FROM npanxx_staging;
-- Step 4: Cleanup
DROP TABLE npanxx_staging;
Strategy 4: Dual Version (Test Before Switching)
-- Maintain two complete tables
CREATE TABLE npanxx_current LIKE npanxx;
CREATE TABLE npanxx_next LIKE npanxx;
-- Load current production data into npanxx_current
-- Load new quarterly data into npanxx_next
-- Test and validate npanxx_next
SELECT COUNT(*) FROM npanxx_next;
SELECT * FROM npanxx_next WHERE Status = 'Active' LIMIT 100;
-- When ready, atomic switch via view or table rename
RENAME TABLE
npanxx TO npanxx_old,
npanxx_next TO npanxx;
-- Rollback if issues discovered
RENAME TABLE
npanxx TO npanxx_next,
npanxx_old TO npanxx;
Typical Changes Per Quarterly Update:
Automated Update Script Template:
#!/bin/bash
# quarterly_update.sh - Automated NPANXX database update
# Configuration
DB_NAME="npanxx_db"
BACKUP_DIR="/backups"
DATA_FILE="/downloads/npanxx_Q1_2025.csv"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Step 1: Backup current database
echo "Creating backup..."
mysqldump $DB_NAME npanxx > "$BACKUP_DIR/npanxx_backup_$TIMESTAMP.sql"
# Step 2: Update database
echo "Importing new data..."
mysql $DB_NAME << EOF
TRUNCATE TABLE npanxx;
LOAD DATA LOCAL INFILE '$DATA_FILE' INTO TABLE npanxx ...;
ANALYZE TABLE npanxx;
EOF
# Step 3: Verify record count
RECORD_COUNT=$(mysql -N $DB_NAME -e "SELECT COUNT(*) FROM npanxx;")
echo "Import complete: $RECORD_COUNT records"
if [ $RECORD_COUNT -lt 500000 ]; then
echo "ERROR: Record count too low, restoring backup"
mysql $DB_NAME < "$BACKUP_DIR/npanxx_backup_$TIMESTAMP.sql"
exit 1
fi
echo "Update successful!"
For applications requiring continuous availability (24/7 operations, real-time APIs), use table swapping (blue-green deployment) to update the database with zero service interruption. The switch happens in milliseconds.
Blue-Green Table Swap Strategy:
Step 1: Prepare New Table (Background, No Impact)
-- Create new table with identical structure
CREATE TABLE npanxx_new LIKE npanxx;
-- Copy indexes from production table
SHOW CREATE TABLE npanxx;
-- Apply same indexes to npanxx_new
-- Import quarterly data into new table (production unaffected)
LOAD DATA LOCAL INFILE '/path/to/npanxx_Q1_2025.csv'
INTO TABLE npanxx_new
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\r\n'
IGNORE 1 ROWS;
-- Rebuild indexes on new table
ANALYZE TABLE npanxx_new;
-- Validate new data (while production continues serving requests)
SELECT COUNT(*) FROM npanxx_new;
SELECT * FROM npanxx_new WHERE Status = 'Active' LIMIT 1000;
-- Run validation queries to ensure data quality
Step 2: Atomic Swap (Sub-Second Downtime)
-- Rename tables atomically (blocks for ~100ms)
RENAME TABLE
npanxx TO npanxx_old,
npanxx_new TO npanxx;
-- Production now uses new data instantly
-- Old table preserved as npanxx_old for rollback
Step 3: Verify & Cleanup
-- Verify production is using new data
SELECT COUNT(*) FROM npanxx;
SELECT MAX(NXXIntroVersion) FROM npanxx; -- Should show recent dates
-- Monitor application for 24-48 hours
-- If everything stable, drop old table
DROP TABLE npanxx_old;
-- If issues discovered, instant rollback:
RENAME TABLE
npanxx TO npanxx_new,
npanxx_old TO npanxx;
Alternative: Database Views (Application Transparency)
-- One-time setup: Use view instead of direct table access
CREATE VIEW npanxx_live AS SELECT * FROM npanxx_blue;
-- Application queries view, not table:
-- SELECT * FROM npanxx_live WHERE NPANXX = '212555';
-- Quarterly update process:
-- Step 1: Load new data into inactive table
LOAD DATA INTO npanxx_green ...;
-- Step 2: Switch view to new table (instant)
CREATE OR REPLACE VIEW npanxx_live AS SELECT * FROM npanxx_green;
-- Step 3: Next quarter, switch back to blue
CREATE OR REPLACE VIEW npanxx_live AS SELECT * FROM npanxx_blue;
-- Ping-pong between blue and green tables each quarter
Complete Blue-Green Deployment Script:
#!/bin/bash
# zero_downtime_update.sh
DB="npanxx_db"
NEW_DATA="/downloads/npanxx_Q1_2025.csv"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
echo "[$TIMESTAMP] Starting zero-downtime update..."
# Step 1: Create and populate new table (no production impact)
echo "Creating new table..."
mysql $DB << EOF
CREATE TABLE npanxx_new LIKE npanxx;
LOAD DATA LOCAL INFILE '$NEW_DATA'
INTO TABLE npanxx_new
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\r\n' IGNORE 1 ROWS;
CREATE INDEX idx_npanxx ON npanxx_new(NPANXX);
CREATE INDEX idx_npa ON npanxx_new(NPA);
CREATE INDEX idx_zipcode ON npanxx_new(ZipCode);
CREATE INDEX idx_state ON npanxx_new(State);
ANALYZE TABLE npanxx_new;
EOF
# Step 2: Validate new data
NEW_COUNT=$(mysql -N $DB -e "SELECT COUNT(*) FROM npanxx_new;")
echo "New table record count: $NEW_COUNT"
if [ $NEW_COUNT -lt 500000 ]; then
echo "ERROR: Record count too low, aborting"
mysql $DB -e "DROP TABLE npanxx_new;"
exit 1
fi
# Step 3: Atomic swap
echo "Performing atomic table swap..."
mysql $DB -e "RENAME TABLE npanxx TO npanxx_old, npanxx_new TO npanxx;"
# Step 4: Verify production
PROD_COUNT=$(mysql -N $DB -e "SELECT COUNT(*) FROM npanxx;")
echo "Production table record count: $PROD_COUNT"
if [ $PROD_COUNT -eq $NEW_COUNT ]; then
echo "? Update successful! Production now using Q1 2025 data"
echo " Old table preserved as npanxx_old for 48h monitoring"
else
echo "ERROR: Count mismatch, rolling back..."
mysql $DB -e "RENAME TABLE npanxx TO npanxx_new, npanxx_old TO npanxx;"
exit 1
fi
Downtime Comparison:
| Method | Typical Downtime | Query Blocking |
|---|---|---|
| TRUNCATE + LOAD (naive) | 10-15 minutes | Full outage |
| DELETE + INSERT (naive) | 15-20 minutes | Full outage |
| Table swap (RENAME TABLE) | ~100ms | Brief lock during rename |
| View switch | ~50ms | No blocking |
Production Considerations:
Testing Your Swap: Before running in production, practice the table swap with a test database. Time the RENAME operation under load to understand exact behavior. Most systems complete the swap in 50-200ms regardless of table size.
Yes, FTP/SFTP access is available upon request for automated quarterly retrieval. This eliminates manual download steps and enables fully automated update pipelines.
FTP Access Details:
Automated Download Script (Bash + SFTP):
#!/bin/bash
# auto_download_npanxx.sh - Automated quarterly FTP retrieval
# Configuration
SFTP_HOST="ftp.zip-codes.com"
SFTP_USER="your_username"
SFTP_PASS="your_password"
REMOTE_DIR="/npanxx/deluxe"
LOCAL_DIR="/data/npanxx/downloads"
TIMESTAMP=$(date +%Y%m%d)
# Create local directory if needed
mkdir -p "$LOCAL_DIR"
# Download latest files via SFTP
sshpass -p "$SFTP_PASS" sftp -oBatchMode=no "$SFTP_USER@$SFTP_HOST" << EOF
cd $REMOTE_DIR
lcd $LOCAL_DIR
mget *.zip
bye
EOF
# Verify download
if [ $? -eq 0 ]; then
echo "[$TIMESTAMP] Download successful"
# Unzip files
cd "$LOCAL_DIR"
unzip -o "*.zip"
# Trigger database update script
/scripts/quarterly_update.sh
else
echo "[$TIMESTAMP] ERROR: Download failed"
exit 1
fi
Python Automated Download:
#!/usr/bin/env python3
# auto_download_npanxx.py
import ftplib
import os
from datetime import datetime
# Configuration
FTP_HOST = 'ftp.zip-codes.com'
FTP_USER = 'your_username'
FTP_PASS = 'your_password'
REMOTE_DIR = '/npanxx/deluxe'
LOCAL_DIR = '/data/npanxx/downloads'
def download_latest_files():
"""Download latest NPANXX files via FTP"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
try:
# Connect to FTP server
ftp = ftplib.FTP(FTP_HOST)
ftp.login(FTP_USER, FTP_PASS)
ftp.cwd(REMOTE_DIR)
# List files
files = ftp.nlst()
print(f"[{timestamp}] Found {len(files)} files on server")
# Download each file
os.makedirs(LOCAL_DIR, exist_ok=True)
for filename in files:
if filename.endswith('.zip') or filename.endswith('.csv'):
local_path = os.path.join(LOCAL_DIR, filename)
print(f"Downloading {filename}...")
with open(local_path, 'wb') as f:
ftp.retrbinary(f'RETR {filename}', f.write)
print(f"? {filename} downloaded")
ftp.quit()
print(f"[{timestamp}] Download complete")
return True
except Exception as e:
print(f"[{timestamp}] ERROR: {str(e)}")
return False
if __name__ == '__main__':
if download_latest_files():
# Trigger database update
os.system('/scripts/quarterly_update.sh')
else:
exit(1)
Scheduled Automation (Cron Job):
# Add to crontab: crontab -e
# Run on the 5th of every month at 2:00 AM (after new data is posted)
0 2 5 * * /scripts/auto_download_npanxx.sh >> /var/log/npanxx_download.log 2>&1
# Alternative: Weekly check (Sundays at 3 AM)
0 3 * * 0 /scripts/auto_download_npanxx.sh >> /var/log/npanxx_download.log 2>&1
Complete Automated Pipeline Example:
#!/bin/bash
# complete_update_pipeline.sh - Full end-to-end automation
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/npanxx_pipeline_$TIMESTAMP.log"
{
echo "[$TIMESTAMP] Starting automated update pipeline"
# Step 1: Download latest files
echo "Step 1: Downloading from FTP..."
/scripts/auto_download_npanxx.sh
if [ $? -ne 0 ]; then
echo "ERROR: Download failed"
exit 1
fi
# Step 2: Validate downloaded files
echo "Step 2: Validating files..."
FILE_SIZE=$(stat -f%z "/data/npanxx/downloads/npanxx.csv")
if [ $FILE_SIZE -lt 100000000 ]; then # Less than 100MB
echo "ERROR: File size too small ($FILE_SIZE bytes)"
exit 1
fi
# Step 3: Backup current database
echo "Step 3: Backing up current database..."
mysqldump npanxx_db npanxx > "/backups/npanxx_backup_$TIMESTAMP.sql"
# Step 4: Import new data (zero-downtime)
echo "Step 4: Importing new data..."
/scripts/zero_downtime_update.sh
# Step 5: Verify update
echo "Step 5: Verifying update..."
RECORD_COUNT=$(mysql -N npanxx_db -e "SELECT COUNT(*) FROM npanxx;")
echo "New record count: $RECORD_COUNT"
# Step 6: Send notification
echo "Step 6: Sending completion notification..."
echo "NPANXX database updated: $RECORD_COUNT records" | \
mail -s "NPANXX Update Complete" admin@yourcompany.com
echo "[$TIMESTAMP] Pipeline complete!"
} | tee "$LOG_FILE"
File Organization on FTP Server:
/npanxx/
+-- npanxx-standard/
¦ +-- {YYYY}-{MM}-StandardAreaCodeDatabase-csv.zip
¦ +-- {YYYY}-{MM}-StandardAreaCodeDatabase-mdb.zip
¦ +-- {YYYY}-{MM}-StandardAreaCodeDatabase-tab.zip
¦ +-- ...
+-- npanxx-deluxe/
¦ +-- {YYYY}-{MM}-DeluxeAreaCodeDatabase-csv.zip
¦ +-- {YYYY}-{MM}-DeluxeAreaCodeDatabase-mdb.zip
¦ +-- {YYYY}-{MM}-DeluxeAreaCodeDatabase-tab.zip
+-- ...
# Each zip contains: CSV, TAB, and MDB formats
Yes, you receive email notifications when each quarterly update is released. Notifications are sent to the email address associated with your account, typically within 24 hours of the new data being posted.
Notification Content:
Notification Timing:
| Quarter | Typical Release Window | Email Notification |
|---|---|---|
| Q1 (Jan-Mar) | Last week of April | Within 24 hours of posting |
| Q2 (Apr-Jun) | Last week of July | Within 24 hours of posting |
| Q3 (Jul-Sep) | Last week of October | Within 24 hours of posting |
| Q4 (Oct-Dec) | Last week of January | Within 24 hours of posting |
Managing Notifications:
info@zip-codes.com to your email whitelist/safe sender list to ensure deliveryWebhook Integration (For Automated Workflows):
While we don't currently offer webhook callbacks, you can achieve automated notification handling using these approaches:
Approach 1: Email-to-Webhook Service
# Use services like Zapier, IFTTT, or Mailgun to convert emails to webhooks
# Example Zapier workflow:
1. Trigger: New email from info@zip-codes.com with subject "Q* Database"
2. Filter: Subject contains "NPA-NXX" or "Area Code"
3. Action: POST to your webhook endpoint
4. Action: Execute download script
# Your webhook endpoint receives:
POST /webhooks/npanxx-update
{
"quarter": "Q1 2025",
"release_date": "2025-04-05",
"edition": "Deluxe"
}
Approach 2: Scheduled Polling
#!/usr/bin/env python3
# poll_for_updates.py - Check FTP for new files periodically
import ftplib
import json
import os
from datetime import datetime
LAST_CHECK_FILE = '/var/lib/npanxx/last_update.json'
def check_for_updates():
"""Poll FTP server for new files"""
# Load last known file list
if os.path.exists(LAST_CHECK_FILE):
with open(LAST_CHECK_FILE, 'r') as f:
last_files = json.load(f)
else:
last_files = []
# Check FTP for current files
ftp = ftplib.FTP('ftp.zip-codes.com')
ftp.login('username', 'password')
ftp.cwd('/npanxx/deluxe')
current_files = ftp.nlst()
ftp.quit()
# Detect new files
new_files = set(current_files) - set(last_files)
if new_files:
print(f"New update detected: {new_files}")
# Trigger your update pipeline
os.system('/scripts/complete_update_pipeline.sh')
# Update last check record
with open(LAST_CHECK_FILE, 'w') as f:
json.dump(current_files, f)
return True
else:
print("No new updates available")
return False
if __name__ == '__main__':
check_for_updates()
# Schedule in cron: Daily check at 6 AM
# 0 6 * * * /scripts/poll_for_updates.py
info@zip-codes.com is whitelisted.
For more details on FTP automation to eliminate dependence on email notifications, see the previous FAQ, Can I automate downloads via FTP or SFTP?
The WeightedLatitude and WeightedLongitude fields (Deluxe edition only) provide population-adjusted coordinates that account for multi-ZIP NPA-NXX distributions. They offer more accurate geographic representation than standard rate center coordinates.
The Problem Weighted Coordinates Solve:
Telephone exchange boundaries and ZIP code boundaries don't align perfectly. A single NPA-NXX block can serve multiple ZIP codes with different population densities. The standard Latitude/Longitude represents the rate center's base location, which may not reflect where most phone numbers in that block are actually located.
Calculation Methodology:
// Weighted coordinate formula
WeightedLatitude = S(ZIP_Latitude × ZipCodeCount) / S(ZipCodeCount)
WeightedLongitude = S(ZIP_Longitude × ZipCodeCount) / S(ZipCodeCount)
// Example: NPA-NXX 212-555 serves 3 ZIP codes
ZIP 10001: Lat 40.7580, Pop 5,423
ZIP 10002: Lat 40.7162, Pop 1,876
ZIP 10003: Lat 40.7310, Pop 987
// Standard coordinate (rate center)
Latitude = 40.7580 (rate center location)
// Weighted coordinate (population center)
WeightedLatitude = (40.7580×5423 + 40.7162×1876 + 40.7310×987) / (5423+1876+987)
WeightedLatitude = 40.7475 (shifted toward population density)
Visual Comparison:
| Scenario | Standard Coordinates | Weighted Coordinates |
|---|---|---|
| Single ZIP NPA-NXX serves only one ZIP |
Accurate representation | Identical to standard (no adjustment needed) |
| Balanced Multi-ZIP Population evenly distributed |
Reasonably accurate | Slight improvement (~0.2-0.5km shift) |
| Skewed Multi-ZIP 80% population in one ZIP |
May be off by 2-5km | Significantly more accurate (shifts toward density) |
| Wide Geographic Spread NPA-NXX spans large area |
May be off by 10-20km | Much better representation of customer density |
When to Use Standard vs Weighted Coordinates:
Use Standard Coordinates (Latitude, Longitude) for:
Use Weighted Coordinates (WeightedLatitude, WeightedLongitude) for:
Implementation Examples:
-- Query 1: Map customer density (use weighted)
SELECT
NPANXX,
WeightedLatitude,
WeightedLongitude,
SUM(ZipCodeCount) as total_population
FROM npanxx
WHERE Status = 'Active'
GROUP BY NPANXX
ORDER BY total_population DESC
LIMIT 100;
-- Query 2: Find NPA-NXX near a location (use weighted for customer proximity)
SELECT
NPANXX,
City,
State,
SQRT(
POW(WeightedLatitude - 40.7128, 2) +
POW(WeightedLongitude - (-74.0060), 2)
) * 69 AS approx_miles -- Rough conversion to miles
FROM npanxx
WHERE Status = 'Active' AND ZipCodeFreq = -1
AND WeightedLatitude != 0 AND WeightedLongitude != 0
HAVING approx_miles < 50
ORDER BY approx_miles;
-- Query 3: Infrastructure planning (use standard for rate center)
SELECT
RateCenter,
Latitude,
Longitude,
COUNT(*) as npanxx_count
FROM npanxx
WHERE Status = 'Active'
GROUP BY RateCenter, Latitude, Longitude;
Accuracy Improvement Statistics:
SELECT Latitude, Longitude, WeightedLatitude, WeightedLongitude FROM npanxx WHERE ...
Remember: Both coordinate sets use 0 to indicate unknown location (never empty string). Always check for WHERE WeightedLatitude != 0 AND WeightedLongitude != 0 before using in calculations.