The basic list that commercial sites start with is probably the same one you started with.
Be aware, the data in here needs LOTS of clean-up.
http://ftp.census.gov/geo/www/gazetteer/places.html
Some of the commercial applications use this data as is. If your needs are more than casual, it is well worth the money if you can find a decent provider service to subscribe to who takes the signifigant amount of effort necessary keeping the data clean and up to date.
How to identify such a provider is something I have not figured out. When I found problems in my records, I did the same searches on some of the demo pages on the commercial sites and found SOME of them had the same issues.
The ones that did not; I have no idea whether their clean up methods were any better than my method of 'best guessing'. I have met one person who sells such a service. He does keep it up to date, but also by using a best guess method (no other way, really). I never got the opportunity to ask him how he went about cleaning the original data.
The haversine formula works GREAT in MySql, it also works well in PHP but I moved the calculation to MySql for performanc considerations. This is the SQL statement I use to get the distance between two zip codes.
It accepts a lat and lon variable passed in from PHP ($l1 and $o1) and compares it to zip codes selected from the database for the member whos ID matches the variable passed in as $locate:
$sql="select
(7912 atan2(sqrt(pow((sin(((radians($l1))-(radians(z.lat)))/2)),2)+ cos((radians(z.lat))) cos((radians($l1))) pow((sin(((radians($o1))-(radians(z.lon)))/2)),2)), sqrt(1-(pow((sin(((radians($l1))-(radians(z.lat)))/2)),2)+ cos((radians(z.lat))) cos((radians($l1))) * pow((sin(((radians($o1))-(radians(z.lon)))/2)),2)))))
as distance
from zipcode_all z, member m
where $locate=m.member_id && m.zipcode=z.zipcode";