Have you ever wanted to add functionality that allows you to perform simple radius searches based on Zip Code? For example, Show me all the cities within 20 miles of a certain zip code or show me the 5 closest retail locations. You can easily add this functionality to your database by importing a zip code database and writing a little code. This article will show you where to get zip code data, how to import it to your database, and how to use the data to perform proximity searches.

Denis and I decided to show you how you can do simple radius searches based on Zip Codes, he did the SQL 2008 version here: http://blogs.lessthandot.com/index.php/DataMgmt/DataDesign/sql-server-2008-proximity-search-with-th (using the geography data type) and I did the SQL Server 2000/2005 version.

The first thing we need to do is load our data. There are various sources for this data, ranging from free to expensive monthly subscriptions. One source of free data is: http://download.geonames.org/export/zip/

** Note: Based on a comment from Steve_O, the format of the geonames file recently changed. I have no control over those changes, and am not notified when changes occur. If you have difficulty loading the data from the file in to the table, you may need to investigate the structure of the data to see if anything has changed.

Once you have downloaded your data, the next step is to import it in to your database. You can use the following script to do it.

If Exists(Select * 
          From   Information_Schema.Tables 
          Where  Table_Name = 'ZipCodes' 
                 And Table_Type = 'Base Table')
  Drop Table ZipCodes

CREATE TABLE [dbo].[ZipCodes](
[country code] [varchar](2) NULL,
[postal code] [varchar](20) NULL,
[place name] [varchar](180) NULL,
[admin name1] [varchar](100) NULL,
[admin code1] [varchar](20) NULL,
[admin name2] [varchar](100) NULL,
[admin code2] [varchar](20) NULL,
[admin name3] [varchar](100) NULL,
[admin code3] [varchar](20) NULL,
[latitude] [decimal](8, 4) NULL,
[longitude] [decimal](8, 4) NULL,
[accuracy] [int] NULL
)

DECLARE @bulk_cmd varchar(1000)
SET @bulk_cmd = 'BULK INSERT ZipCodes
    FROM ''C:YourFolderUS.txt''
    WITH (FIELDTERMINATOR=''t'', ROWTERMINATOR = '''+CHAR(10)+''')'

EXEC(@bulk_cmd)

Alter Table ZipCodes Drop Column Unused1, Unused2, Unused3

CREATE CLUSTERED INDEX IX_ZipCodes_Zip ON dbo.ZipCodes(ZipCode, Longitude, Latitude)
GO
CREATE NONCLUSTERED INDEX IX_ZipCodes_Longitude_Latitude ON dbo.ZipCodes(Longitude,Latitude, ZipCode)

GO

Select * From ZipCodes

Next, we’ll need to add a couple of functions that will allow us to use this data.

The original function that calculates distances has been replaced with this one. There was a flaw in the original version. Based on rounding errors, the calculations fed in to the arc-cosine (acos) function could be greater than 1 or less than -1 which would cause a domain error. To accommodate this flaw, I perform the calculations in multiple steps so that I can catch the values that result in the domain error.

Credit for finding the flaw goes to Chris. Thank you.

CREATE Function [dbo].[CalculateDistance]
	(@Longitude1 Decimal(8,5), 
	@Latitude1   Decimal(8,5),
	@Longitude2  Decimal(8,5),
	@Latitude2   Decimal(8,5))
Returns Float
As
Begin
Declare @Temp Float

Set @Temp = sin(@Latitude1/57.2957795130823) * sin(@Latitude2/57.2957795130823) + cos(@Latitude1/57.2957795130823) * cos(@Latitude2/57.2957795130823) * cos(@Longitude2/57.2957795130823 - @Longitude1/57.2957795130823)

if @Temp > 1 
	Set @Temp = 1
Else If @Temp < -1
	Set @Temp = -1

Return (3958.75586574 * acos(@Temp)	) 

End
Create Function [dbo].[LatitudePlusDistance](@StartLatitude Float, @Distance Float) Returns Float
As
Begin
    Return (Select @StartLatitude + Sqrt(@Distance * @Distance / 4766.8999155991))
End
Create Function [dbo].[LongitudePlusDistance]
    (@StartLongitude Float,
    @StartLatitude Float,
    @Distance Float)
Returns Float
AS
Begin
    Return (Select @StartLongitude + Sqrt(@Distance * @Distance / (4784.39411916406 * Cos(2 * @StartLatitude / 114.591559026165) * Cos(2 * @StartLatitude / 114.591559026165))))
End

Finally, we can begin writing some interesting queries. For example, return a list of zipcodes within 20 miles of zipcode 20013 (Washington, DC).

-- Declare some variables that we will need.
Declare @Longitude Decimal(8,5),
        @Latitude Decimal(8,5),
        @MinLongitude Decimal(8,5),
        @MaxLongitude Decimal(8,5),
        @MinLatitude Decimal(8,5),
        @MaxLatitude Decimal(8,5)

-- Get the lat/long for the given zip
Select @Longitude = Longitude,
       @Latitude = Latitude
From   ZipCodes
Where  ZipCode = '20013'

-- Calculate the Max Lat/Long
Select @MaxLongitude = dbo.LongitudePlusDistance(@Longitude, @Latitude, 20),
       @MaxLatitude = dbo.LatitudePlusDistance(@Latitude, 20)

-- Calculate the min lat/long
Select @MinLatitude = 2 * @Latitude - @MaxLatitude,
       @MinLongitude = 2 * @Longitude - @MaxLongitude

-- The query to return all zips within a certain distance
Select ZipCode
From   ZipCodes
Where  Longitude Between @MinLongitude And @MaxLongitude
       And Latitude Between @MinLatitude And @MaxLatitude
       And dbo.CalculateDistance(@Longitude, @Latitude, Longitude, Latitude) <= 20

You could also use this for a “store locator”. On a website, potential customers could enter their zipcode and you could present a list of top 5 closest stores.
The query shown below assumes you have a Stores table with a ZipCode column.

Declare @Longitude Decimal(8,5)
Declare @Latitude Decimal(8,5)

Select	@Longitude = Longitude,
		@Latitude = Latitude
From	ZipCodes
Where	ZipCode = '20013'

Select  Top 5 Stores.StoreName, ZipCodes.City,  dbo.CalculateDistance(@Longitude, @Latitude, ZipCodes.Longitude, ZipCodes.Latitude) As Distance 
From	@Stores As Stores
		Inner Join ZipCodes 
			On Stores.ZipCode = ZipCodes.ZipCode
Order By dbo.CalculateDistance(@Longitude, @Latitude, ZipCodes.Longitude, ZipCodes.Latitude)

As you can see, these queries perform very well, and are relatively easy to write.