From d4ff0e46da7f17961e4468288b8f534e6b248a9c Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Wed, 6 Oct 2021 17:43:29 -0700 Subject: [PATCH 1/6] Fix batch response return --- api.go | 44 ++++++++++++----------------- geocode.go | 74 ++++++++++++++++++++++++++++++++++++++++++------- geocode_test.go | 16 ++++++++--- reverse.go | 16 ++++++----- 4 files changed, 102 insertions(+), 48 deletions(-) diff --git a/api.go b/api.go index e52bae9..1927cf1 100644 --- a/api.go +++ b/api.go @@ -21,18 +21,22 @@ const ( MethodPost = "POST" ) -func (g *Geocodio) get(path string, query map[string]string) (GeocodeResult, error) { - return g.call(MethodGet, path, nil, query) +type saver interface { + SaveDebug(requestedURL, status string, statusCode int, body []byte) } -func (g *Geocodio) post(path string, payload interface{}, query map[string]string) (GeocodeResult, error) { - return g.call(MethodPost, path, payload, query) +func (g *Geocodio) get(path string, query map[string]string, result saver) error { + return g.call(MethodGet, path, nil, query, result) } -func (g *Geocodio) call(method, path string, payload interface{}, query map[string]string) (GeocodeResult, error) { +func (g *Geocodio) post(path string, payload interface{}, query map[string]string, result saver) error { + return g.call(MethodPost, path, payload, query, result) +} + +func (g *Geocodio) call(method, path string, payload interface{}, query map[string]string, result saver) error { if strings.Index(path, "/") != 0 { - return GeocodeResult{}, errors.New("Path must start with a forward slash: ' / ' ") + return errors.New("Path must start with a forward slash: ' / ' ") } rawURL := GeocodioAPIBaseURLv1 + path + "?api_key=" + g.APIKey @@ -50,7 +54,7 @@ func (g *Geocodio) call(method, path string, payload interface{}, query map[stri u, err := url.Parse(rawURL) if err != nil { - return GeocodeResult{}, nil + return nil } if query != nil { @@ -72,7 +76,7 @@ func (g *Geocodio) call(method, path string, payload interface{}, query map[stri if payload != nil { body, err := json.Marshal(payload) if err != nil { - return GeocodeResult{}, err + return err } req.Body = ioutil.NopCloser(bytes.NewReader(body)) @@ -82,35 +86,21 @@ func (g *Geocodio) call(method, path string, payload interface{}, query map[stri resp, err := client.Do(&req) if err != nil { - return GeocodeResult{}, err + return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return GeocodeResult{}, err + return err } - - result := GeocodeResult{} - - result.Debug.RequestedURL = u.String() - result.Debug.Status = resp.Status - result.Debug.StatusCode = resp.StatusCode - result.Debug.RawResponse = body + result.SaveDebug(u.String(), resp.Status, resp.StatusCode, body) err = json.Unmarshal(body, &result) if err != nil { - return result, err - } - - if len(result.Results) > 0 { - if result.Results[0].Error != nil { - if result.Results[0].Error.Message != "" { - return result, errors.New(result.Results[0].Error.Message) - } - } + return err } - return result, err + return nil } diff --git a/geocode.go b/geocode.go index 20fe1cb..f1f95eb 100644 --- a/geocode.go +++ b/geocode.go @@ -5,6 +5,29 @@ import ( "strings" ) +// BatchResponse +type BatchResponse struct { + Results []BatchResult `json:"results"` + Debug struct { + RawResponse []byte `json:"-"` + RequestedURL string `json:"requested_url"` + Status string `json:"status"` + StatusCode int `json:"status_code"` + } `json:"-"` +} + +// BatchResult +type BatchResult struct { + Query string `json:"query"` + Response BatchResultItem `json:"response"` +} + +type BatchResultItem struct { + Input Input `json:"input,omitempty"` + Results []Address `json:"results"` + Error string `json:"error,omitempty"` +} + // GeocodeResponse type GeocodeResult struct { Input Input `json:"input,omitempty"` @@ -27,19 +50,48 @@ type Result struct { Error *ErrorResponse `json:"response,omitempty"` } +func (self *GeocodeResult) SaveDebug(requestedURL, status string, statusCode int, body []byte) { + self.Debug.RequestedURL = requestedURL + self.Debug.Status = status + self.Debug.StatusCode = statusCode + self.Debug.RawResponse = body +} + +func (self *GeocodeResult) Error() string { + if len(self.Results) > 0 { + if self.Results[0].Error != nil { + return self.Results[0].Error.Message + } + } + return "" +} + // ResponseAsString helper to return raw response func (self *GeocodeResult) ResponseAsString() string { return string(self.Debug.RawResponse) } +func (self *BatchResponse) SaveDebug(requestedURL, status string, statusCode int, body []byte) { + self.Debug.RequestedURL = requestedURL + self.Debug.Status = status + self.Debug.StatusCode = statusCode + self.Debug.RawResponse = body +} + +// ResponseAsString helper to return raw response +func (self *BatchResponse) ResponseAsString() string { + return string(self.Debug.RawResponse) +} + // Geocode single address // See: http://geocod.io/docs/#toc_4 func (g *Geocodio) Geocode(address string) (GeocodeResult, error) { + resp := GeocodeResult{} if address == "" { - return GeocodeResult{}, ErrAddressIsEmpty + return resp, ErrAddressIsEmpty } - resp, err := g.get("/geocode", map[string]string{"q": address}) + err := g.get("/geocode", map[string]string{"q": address}, &resp) if err != nil { return GeocodeResult{}, err } @@ -52,15 +104,16 @@ func (g *Geocodio) Geocode(address string) (GeocodeResult, error) { } // GeocodeBatch look up addresses -func (g *Geocodio) GeocodeBatch(addresses ...string) (GeocodeResult, error) { +func (g *Geocodio) GeocodeBatch(addresses ...string) (BatchResponse, error) { + resp := BatchResponse{} if len(addresses) == 0 { - return GeocodeResult{}, ErrBatchAddressesIsEmpty + return resp, ErrBatchAddressesIsEmpty } // TODO: support limit - resp, err := g.post("/geocode", addresses, nil) + err := g.post("/geocode", addresses, nil, &resp) if err != nil { - return GeocodeResult{}, err + return BatchResponse{}, err } if len(resp.Results) == 0 { @@ -99,19 +152,20 @@ func (g *Geocodio) GeocodeAndReturnStateLegislativeDistricts(address string) (Ge Each field counts as an additional lookup each */ func (g *Geocodio) GeocodeReturnFields(address string, fields ...string) (GeocodeResult, error) { + resp := GeocodeResult{} if address == "" { - return GeocodeResult{}, errors.New("address can not be empty") + return resp, errors.New("address can not be empty") } fieldsCommaSeparated := strings.Join(fields, ",") - resp, err := g.get("/geocode", + err := g.get("/geocode", map[string]string{ "q": address, "fields": fieldsCommaSeparated, - }) + }, &resp) if err != nil { - return GeocodeResult{}, err + return resp, err } if len(resp.Results) == 0 { diff --git a/geocode_test.go b/geocode_test.go index f4542fe..58865b7 100644 --- a/geocode_test.go +++ b/geocode_test.go @@ -307,14 +307,22 @@ func TestGeocodeInvalidNoResults(t *testing.T) { if err != geocodio.ErrNoResultsFound { t.Error("Expected error", geocodio.ErrNoResultsFound.Error(), "but saw", err.Error()) } +} - resp, err = gc.GeocodeBatch("123 Nonsense Ln, Nowhere, XX") - if err == nil { +func TestGeocodeBatchInvalidNoResults(t *testing.T) { + gc, err := geocodio.New() + if err != nil { + t.Error("Failed with API KEY set.", err) + } + + resp, err := gc.GeocodeBatch("123 Nonsense Ln, Nowhere, XX") + if err != nil { + t.Error("Expected success", err) + } + if resp.Results[0].Response.Error == "" { t.Error("Expected to see an error") fmt.Println(resp.ResponseAsString()) - return } - } // TODO: School District (school) diff --git a/reverse.go b/reverse.go index a7ad23a..e712bcc 100644 --- a/reverse.go +++ b/reverse.go @@ -19,9 +19,10 @@ func (g *Geocodio) Reverse(latitude, longitude float64) (GeocodeResult, error) { latStr := strconv.FormatFloat(latitude, 'f', 9, 64) lngStr := strconv.FormatFloat(longitude, 'f', 9, 64) - resp, err := g.get("/reverse", map[string]string{"q": latStr + "," + lngStr}) + resp := GeocodeResult{} + err := g.get("/reverse", map[string]string{"q": latStr + "," + lngStr}, &resp) if err != nil { - return GeocodeResult{}, err + return resp, err } if len(resp.Results) == 0 { @@ -44,13 +45,14 @@ func (g *Geocodio) ReverseGeocode(latitude, longitude float64) (GeocodeResult, e } // ReverseBatch supports a batch lookup by lat/lng coordinate pairs -func (g *Geocodio) ReverseBatch(latlngs ...float64) (GeocodeResult, error) { +func (g *Geocodio) ReverseBatch(latlngs ...float64) (BatchResponse, error) { + resp := BatchResponse{} if len(latlngs) == 0 { - return GeocodeResult{}, ErrReverseBatchMissingCoords + return resp, ErrReverseBatchMissingCoords } if len(latlngs)%2 == 1 { - return GeocodeResult{}, ErrReverseBatchInvalidCoordsPairs + return resp, ErrReverseBatchInvalidCoordsPairs } var ( @@ -72,9 +74,9 @@ func (g *Geocodio) ReverseBatch(latlngs ...float64) (GeocodeResult, error) { pair = coord } - resp, err := g.post("/reverse", payload, nil) + err := g.post("/reverse", payload, nil, &resp) if err != nil { - return GeocodeResult{}, err + return resp, err } if len(resp.Results) == 0 { From fa27d0e9a58bc2c1f619530090f6889ef274cc48 Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Wed, 6 Oct 2021 19:02:19 -0700 Subject: [PATCH 2/6] use sling --- _scripts/env.sh | 0 _scripts/tests_coverage.sh | 0 _scripts/tests_unit.sh | 0 geocodio.go | 3 +++ go.mod | 2 ++ go.sum | 6 ++++++ 6 files changed, 11 insertions(+) mode change 100755 => 100644 _scripts/env.sh mode change 100755 => 100644 _scripts/tests_coverage.sh mode change 100755 => 100644 _scripts/tests_unit.sh create mode 100644 go.sum diff --git a/_scripts/env.sh b/_scripts/env.sh old mode 100755 new mode 100644 diff --git a/_scripts/tests_coverage.sh b/_scripts/tests_coverage.sh old mode 100755 new mode 100644 diff --git a/_scripts/tests_unit.sh b/_scripts/tests_unit.sh old mode 100755 new mode 100644 diff --git a/geocodio.go b/geocodio.go index 1f75f10..3b35e65 100644 --- a/geocodio.go +++ b/geocodio.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "strings" + + "github.com/dghubble/sling" ) const ( @@ -25,6 +27,7 @@ type Input struct { // or passed in as the first string value func New(apiKey ...string) (*Geocodio, error) { + client := sling.New().Base(GeocodioAPIBaseURLv1) key := os.Getenv(EnvGeocodioAPIKey) if strings.TrimSpace(key) == "" { key = os.Getenv(EnvOldAPIKey) diff --git a/go.mod b/go.mod index be0d320..4332f9e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/stevepartridge/geocodio go 1.13 + +require github.com/dghubble/sling v1.4.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7fcf87b --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= +github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From f0164f35c21877ea7840000fabaad1888ce8e61e Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Thu, 7 Oct 2021 07:47:22 -0700 Subject: [PATCH 3/6] switch to sling --- address.go | 14 ------ api.go | 106 ---------------------------------------- census.go | 29 ----------- congressional.go | 59 ----------------------- env.go | 1 - geocode.go | 94 ++++++++++-------------------------- geocode_test.go | 122 ++++++++++------------------------------------- geocodio.go | 58 ++++++++++++---------- geocodio_test.go | 38 --------------- go.mod | 5 +- go.sum | 10 ++++ reverse.go | 28 +++++------ reverse_test.go | 2 - school.go | 8 ---- timezone.go | 10 ---- zip.go | 28 ----------- 16 files changed, 111 insertions(+), 501 deletions(-) delete mode 100644 api.go diff --git a/address.go b/address.go index 915d2dc..311ba1d 100644 --- a/address.go +++ b/address.go @@ -12,20 +12,6 @@ type Address struct { } // Components -/* - "address_components": { - "number": "1109", - "predirectional": "N", - "street": "Highland", - "suffix": "St", - "formatted_street": "N Highland St", - "city": "Arlington", - "county": "Arlington County", - "state": "VA", - "zip": "22201", - "country": "US" - }, -*/ type Components struct { Number string `json:"number"` Street string `json:"street"` diff --git a/api.go b/api.go deleted file mode 100644 index 1927cf1..0000000 --- a/api.go +++ /dev/null @@ -1,106 +0,0 @@ -package geocodio - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - - // "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - "time" -) - -const ( - // MethodGet constant - MethodGet = "GET" - // MethodPost constant - MethodPost = "POST" -) - -type saver interface { - SaveDebug(requestedURL, status string, statusCode int, body []byte) -} - -func (g *Geocodio) get(path string, query map[string]string, result saver) error { - return g.call(MethodGet, path, nil, query, result) -} - -func (g *Geocodio) post(path string, payload interface{}, query map[string]string, result saver) error { - return g.call(MethodPost, path, payload, query, result) -} - -func (g *Geocodio) call(method, path string, payload interface{}, query map[string]string, result saver) error { - - if strings.Index(path, "/") != 0 { - return errors.New("Path must start with a forward slash: ' / ' ") - } - - rawURL := GeocodioAPIBaseURLv1 + path + "?api_key=" + g.APIKey - - if query != nil { - for k, v := range query { - rawURL = fmt.Sprintf("%s&%s=%s", rawURL, k, url.QueryEscape(v)) - } - } - - timeout := time.Duration(10 * time.Second) - client := http.Client{ - Timeout: timeout, - } - - u, err := url.Parse(rawURL) - if err != nil { - return nil - } - - if query != nil { - for k, v := range query { - if u.Query().Get(k) != "" { - u.Query().Set(k, v) - continue - } - u.Query().Add(k, v) - } - } - - req := http.Request{ - Method: method, - URL: u, - Header: http.Header{}, - } - - if payload != nil { - body, err := json.Marshal(payload) - if err != nil { - return err - } - - req.Body = ioutil.NopCloser(bytes.NewReader(body)) - - req.Header.Add("Content-Type", "application/json") - } - - resp, err := client.Do(&req) - if err != nil { - return err - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - result.SaveDebug(u.String(), resp.Status, resp.StatusCode, body) - - err = json.Unmarshal(body, &result) - if err != nil { - return err - } - - return nil -} diff --git a/census.go b/census.go index 745d36f..162a759 100644 --- a/census.go +++ b/census.go @@ -15,35 +15,6 @@ type CensusResults struct { } // Census field -/* -{ - "census_year": 2010, - "state_fips": "51", - "county_fips": "51013", - "tract_code": "101801", - "block_code": "1004", - "block_group": "1", - "full_fips": "510131018011004", - "place": { - "name": "Arlington", - "fips": "5103000" - }, - "metro_micro_statistical_area": { - "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", - "area_code": "47900", - "type": "metropolitan" - }, - "combined_statistical_area": { - "name": "Washington-Baltimore-Northern Virginia, DC-MD-VA-WV", - "area_code": "51548" - }, - "metropolitan_division": { - "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", - "area_code": "47894" - }, - "source": "US Census Bureau" -} -*/ type Census struct { Year int `json:"census_year"` StateFIPS string `json:"state_fips"` diff --git a/congressional.go b/congressional.go index d5b177c..8f72f58 100644 --- a/congressional.go +++ b/congressional.go @@ -1,14 +1,6 @@ package geocodio // Congressional District field -/* -"name": "Congressional District 8", -"district_number": 8, -"congress_number": "116th", -"congress_years": "2019-2021", -"proportion": 1, -"current_legislators": [...] -*/ type CongressionalDistrict struct { Name string `json:"name"` DistrictNumber int `json:"district_number"` @@ -19,16 +11,6 @@ type CongressionalDistrict struct { } // Legislator field -/* -{ - "type": "representative", - "bio": {...}, - "contact": {...}, - "social": {...}, - "references": {...}, - "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" -} -*/ type Legislator struct { Type string `json:"type"` Bio Bio `json:"bio"` @@ -39,15 +21,6 @@ type Legislator struct { } // Bio field -/* - "bio": { - "last_name": "Beyer", - "first_name": "Donald", - "birthday": "1950-06-20", - "gender": "M", - "party": "Democrat" - } -*/ type Bio struct { LastName string `json:"last_name"` FirstName string `json:"first_name"` @@ -57,14 +30,6 @@ type Bio struct { } // Contact field -/* -"contact": { - "url": "https://beyer.house.gov", - "address": "1119 Longworth House Office Building Washington DC 20515-4608", - "phone": "(202) 225-4376", - "contact_form": null -} -*/ type Contact struct { URL string `json:"url"` Address string `json:"address"` @@ -73,15 +38,6 @@ type Contact struct { } // Social field -/* -"social": { - "rss_url": null, - "twitter": "RepDonBeyer", - "facebook": "RepDonBeyer", - "youtube": null, - "youtube_id": "UCPJGVbOVcAVGiBwq8qr_T9w" -} -*/ type CongressionalSocial struct { RSSURL string `json:"rss_url"` Twitter string `json:"twitter"` @@ -91,21 +47,6 @@ type CongressionalSocial struct { } // References field -/* -"references": { - "bioguide_id": "B001292", - "thomas_id": "02272", - "opensecrets_id": "N00036018", - "lis_id": null, - "cspan_id": "21141", - "govtrack_id": "412657", - "votesmart_id": "1707", - "ballotpedia_id": null, - "washington_post_id": null, - "icpsr_id": "21554", - "wikipedia_id": "Don Beyer" -} -*/ type References struct { BioguideID string `json:"bioguide_id"` ThomasID string `json:"thomas_id"` diff --git a/env.go b/env.go index 21f911b..22c57fd 100644 --- a/env.go +++ b/env.go @@ -2,5 +2,4 @@ package geocodio const ( EnvGeocodioAPIKey = "GEOCODIO_API_KEY" - EnvOldAPIKey = "API_KEY" ) diff --git a/geocode.go b/geocode.go index f1f95eb..f812e44 100644 --- a/geocode.go +++ b/geocode.go @@ -8,12 +8,6 @@ import ( // BatchResponse type BatchResponse struct { Results []BatchResult `json:"results"` - Debug struct { - RawResponse []byte `json:"-"` - RequestedURL string `json:"requested_url"` - Status string `json:"status"` - StatusCode int `json:"status_code"` - } `json:"-"` } // BatchResult @@ -32,12 +26,6 @@ type BatchResultItem struct { type GeocodeResult struct { Input Input `json:"input,omitempty"` Results []Result `json:"results"` - Debug struct { - RawResponse []byte `json:"-"` - RequestedURL string `json:"requested_url"` - Status string `json:"status"` - StatusCode int `json:"status_code"` - } `json:"-"` } type ErrorResponse struct { @@ -50,77 +38,44 @@ type Result struct { Error *ErrorResponse `json:"response,omitempty"` } -func (self *GeocodeResult) SaveDebug(requestedURL, status string, statusCode int, body []byte) { - self.Debug.RequestedURL = requestedURL - self.Debug.Status = status - self.Debug.StatusCode = statusCode - self.Debug.RawResponse = body -} - -func (self *GeocodeResult) Error() string { - if len(self.Results) > 0 { - if self.Results[0].Error != nil { - return self.Results[0].Error.Message - } - } - return "" -} - -// ResponseAsString helper to return raw response -func (self *GeocodeResult) ResponseAsString() string { - return string(self.Debug.RawResponse) -} - -func (self *BatchResponse) SaveDebug(requestedURL, status string, statusCode int, body []byte) { - self.Debug.RequestedURL = requestedURL - self.Debug.Status = status - self.Debug.StatusCode = statusCode - self.Debug.RawResponse = body -} - -// ResponseAsString helper to return raw response -func (self *BatchResponse) ResponseAsString() string { - return string(self.Debug.RawResponse) -} - // Geocode single address // See: http://geocod.io/docs/#toc_4 func (g *Geocodio) Geocode(address string) (GeocodeResult, error) { - resp := GeocodeResult{} + res := GeocodeResult{} if address == "" { - return resp, ErrAddressIsEmpty + return res, ErrAddressIsEmpty } - err := g.get("/geocode", map[string]string{"q": address}, &resp) + err := g.do("GET", "/geocode", map[string]string{"q": address}, nil, &res) if err != nil { - return GeocodeResult{}, err + return res, err } - if len(resp.Results) == 0 { - return resp, ErrNoResultsFound + if len(res.Results) == 0 { + return res, ErrNoResultsFound } - return resp, nil + return res, nil } // GeocodeBatch look up addresses func (g *Geocodio) GeocodeBatch(addresses ...string) (BatchResponse, error) { - resp := BatchResponse{} + res := BatchResponse{} if len(addresses) == 0 { - return resp, ErrBatchAddressesIsEmpty + return res, ErrBatchAddressesIsEmpty } // TODO: support limit - err := g.post("/geocode", addresses, nil, &resp) + err := g.do("POST", "/geocode", nil, addresses, &res) if err != nil { - return BatchResponse{}, err + return res, err } - if len(resp.Results) == 0 { - return resp, ErrNoResultsFound + if len(res.Results) == 0 { + return res, ErrNoResultsFound } - return resp, nil + return res, nil } // GeocodeAndReturnTimezone will geocode and include Timezone in the fields response @@ -152,25 +107,24 @@ func (g *Geocodio) GeocodeAndReturnStateLegislativeDistricts(address string) (Ge Each field counts as an additional lookup each */ func (g *Geocodio) GeocodeReturnFields(address string, fields ...string) (GeocodeResult, error) { - resp := GeocodeResult{} + res := GeocodeResult{} if address == "" { - return resp, errors.New("address can not be empty") + return res, errors.New("address can not be empty") } fieldsCommaSeparated := strings.Join(fields, ",") - err := g.get("/geocode", - map[string]string{ - "q": address, - "fields": fieldsCommaSeparated, - }, &resp) + err := g.do("GET", "/geocode", map[string]string{ + "q": address, + "fields": fieldsCommaSeparated, + }, nil, &res) if err != nil { - return resp, err + return res, err } - if len(resp.Results) == 0 { - return resp, ErrNoResultsFound + if len(res.Results) == 0 { + return res, ErrNoResultsFound } - return resp, nil + return res, nil } diff --git a/geocode_test.go b/geocode_test.go index 58865b7..0f2821d 100644 --- a/geocode_test.go +++ b/geocode_test.go @@ -1,125 +1,57 @@ package geocodio_test import ( - "fmt" "testing" "github.com/stevepartridge/geocodio" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGeocodeWithEmptyAddress(t *testing.T) { gc, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } - _, err = gc.Geocode("") - if err == nil { - t.Error("Error should not be nil.") - } -} - -func TestGeocodeDebugResponseAsString(t *testing.T) { - gc, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } - result, err := gc.Geocode(AddressTestOneFull) - if err != nil { - t.Error(err) - } - - if result.ResponseAsString() == "" { - t.Error("Response should be a valid string.") - } + require.NoError(t, err) + _, err = gc.Geocode("") + assert.Error(t, err) } func TestGeocodeFullAddress(t *testing.T) { gc, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } - result, err := gc.Geocode(AddressTestOneFull) - if err != nil { - t.Error(err) - } - - // t.Log(result.ResponseAsString()) + require.NoError(t, err) - if len(result.Results) == 0 { - t.Error("Results length is 0") - } - - if result.Results[0].Location.Latitude != AddressTestOneLatitude { - t.Errorf("Location latitude %f does not match %f", result.Results[0].Location.Latitude, AddressTestOneLatitude) - } - - if result.Results[0].Location.Longitude != AddressTestOneLongitude { - t.Errorf("Location longitude %f does not match %f", result.Results[0].Location.Longitude, AddressTestOneLongitude) - } + result, err := gc.Geocode(AddressTestOneFull) + require.NoError(t, err) + require.True(t, len(result.Results) > 0) + assert.Equal(t, AddressTestOneLatitude, result.Results[0].Location.Latitude) + assert.Equal(t, AddressTestOneLongitude, result.Results[0].Location.Longitude) } func TestGeocodeFullAddressReturningTimezone(t *testing.T) { gc, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } - result, err := gc.GeocodeAndReturnTimezone(AddressTestOneFull) - if err != nil { - t.Error(err) - } - - if len(result.Results) == 0 { - t.Error("Results length is 0") - } - - if result.Results[0].Location.Latitude != AddressTestOneLatitude { - t.Errorf("Location latitude %f does not match %f", result.Results[0].Location.Latitude, AddressTestOneLatitude) - } - - if result.Results[0].Location.Longitude != AddressTestOneLongitude { - t.Errorf("Location longitude %f does not match %f", result.Results[0].Location.Longitude, AddressTestOneLongitude) - } + require.NoError(t, err) - if result.Results[0].Fields.Timezone.Name == "" { - t.Error("Timezone field not found") - } + result, err := gc.GeocodeAndReturnTimezone(AddressTestOneFull) + assert.NoError(t, err) + require.True(t, len(result.Results) > 0) - if !result.Results[0].Fields.Timezone.ObservesDST { - t.Error("Timezone field does not match", result.Results[0].Fields.Timezone) - } + assert.Equal(t, AddressTestOneLatitude, result.Results[0].Location.Latitude) + assert.Equal(t, AddressTestOneLongitude, result.Results[0].Location.Longitude) + assert.NotEmpty(t, result.Results[0].Fields.Timezone.Name) + assert.True(t, result.Results[0].Fields.Timezone.ObservesDST) } func TestGeocodeFullAddressReturningZip4(t *testing.T) { gc, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } + require.NoError(t, err) result, err := gc.GeocodeAndReturnZip4(AddressTestOneFull) - if err != nil { - t.Error(err) - } - - if len(result.Results) == 0 { - t.Error("Results length is 0") - } - - if result.Results[0].Location.Latitude != AddressTestOneLatitude { - t.Errorf("Location latitude %f does not match %f", result.Results[0].Location.Latitude, AddressTestOneLatitude) - } - - if result.Results[0].Location.Longitude != AddressTestOneLongitude { - t.Errorf("Location longitude %f does not match %f", result.Results[0].Location.Longitude, AddressTestOneLongitude) - } - - if len(result.Results[0].Fields.Zip4.Plus4) == 0 { - t.Error("Zip4 field not found") - } + require.NoError(t, err) + require.True(t, len(result.Results) > 0) - // if !result.Results[0].Fields.Timezone.ObservesDST { - // t.Error("Zip4 field does not match", result.Results[0].Fields.Timezone) - // } + assert.Equal(t, AddressTestOneLatitude, result.Results[0].Location.Latitude) + assert.Equal(t, AddressTestOneLongitude, result.Results[0].Location.Longitude) + assert.True(t, len(result.Results[0].Fields.Zip4.Plus4) > 0) } func TestGeocodeFullAddressReturningCongressionalDistrict(t *testing.T) { @@ -298,10 +230,9 @@ func TestGeocodeInvalidNoResults(t *testing.T) { t.Error("Failed with API KEY set.", err) } - resp, err := gc.Geocode("123 Nonsense Ln, Nowhere, XX") + _, err = gc.Geocode("123 Nonsense Ln, Nowhere, XX") if err == nil { t.Error("Expected to see an error") - fmt.Println(resp.ResponseAsString()) return } if err != geocodio.ErrNoResultsFound { @@ -321,7 +252,6 @@ func TestGeocodeBatchInvalidNoResults(t *testing.T) { } if resp.Results[0].Response.Error == "" { t.Error("Expected to see an error") - fmt.Println(resp.ResponseAsString()) } } diff --git a/geocodio.go b/geocodio.go index 3b35e65..5c2ce15 100644 --- a/geocodio.go +++ b/geocodio.go @@ -1,9 +1,10 @@ package geocodio import ( - "fmt" + "net/http" "os" "strings" + "time" "github.com/dghubble/sling" ) @@ -15,7 +16,8 @@ const ( // Geocodio is the base struct type Geocodio struct { - APIKey string + APIKey string `url:"api_key"` + client sling.Doer } type Input struct { @@ -26,13 +28,7 @@ type Input struct { // New creates a Geocodio instance based on an API key in either the environment // or passed in as the first string value func New(apiKey ...string) (*Geocodio, error) { - - client := sling.New().Base(GeocodioAPIBaseURLv1) key := os.Getenv(EnvGeocodioAPIKey) - if strings.TrimSpace(key) == "" { - key = os.Getenv(EnvOldAPIKey) - } - if len(apiKey) == 0 && strings.TrimSpace(key) == "" { return nil, ErrMissingAPIKey } @@ -45,31 +41,43 @@ func New(apiKey ...string) (*Geocodio, error) { return nil, ErrMissingAPIKey } + timeout := time.Duration(10 * time.Second) + client := &http.Client{ + Timeout: timeout, + } + g := Geocodio{ APIKey: key, + client: client, } return &g, nil } -// NewGeocodio is a helper to create new Geocodio reference -// since 1.6+ this is kept for backwards compatiblity -// this is deprecatd and will be removed in 2+ -func NewGeocodio(apiKey string) (*Geocodio, error) { - - fmt.Println(` - NewGeocodio() is deprecated and will be removed in 2+ - Use geocodio.New("YOUR_API_KEY") - or with the environment variable ` + EnvGeocodioAPIKey + ` - Use geocodio.New()`) - - if apiKey == "" { - return nil, ErrMissingAPIKey +func (g *Geocodio) do(method, path string, params map[string]string, bodyJSON, result interface{}) error { + s := sling.New(). + Doer(g.client). + Base(GeocodioAPIBaseURLv1). + QueryStruct(g). + Set("Content-Type", "application/json"). + Path(path). + BodyJSON(bodyJSON) + + req, err := s.Request() + if err != nil { + return err } + req.Method = method + req.URL.RawQuery = getQueryString(req, params) - g := Geocodio{ - APIKey: apiKey, - } + _, err = s.Do(req, result, nil) + return err +} - return &g, nil +func getQueryString(req *http.Request, params map[string]string) string { + query := req.URL.Query() + for key, value := range params { + query.Add(key, value) + } + return query.Encode() } diff --git a/geocodio_test.go b/geocodio_test.go index ead5c48..011c462 100644 --- a/geocodio_test.go +++ b/geocodio_test.go @@ -56,12 +56,7 @@ func TestGeocodioWithApiKey(t *testing.T) { func TestGeocodioWithoutApiKey(t *testing.T) { key := os.Getenv(geocodio.EnvGeocodioAPIKey) - if key == "" && os.Getenv(geocodio.EnvOldAPIKey) != "" { - key = os.Getenv(geocodio.EnvOldAPIKey) - } - os.Setenv(geocodio.EnvGeocodioAPIKey, "") - os.Setenv(geocodio.EnvOldAPIKey, "") _, err := geocodio.New() if err == nil { @@ -72,14 +67,8 @@ func TestGeocodioWithoutApiKey(t *testing.T) { } func TestGeocodioWithoutApiKeyEnvAndEmptyString(t *testing.T) { - key := os.Getenv(geocodio.EnvGeocodioAPIKey) - if key == "" && os.Getenv(geocodio.EnvOldAPIKey) != "" { - key = os.Getenv(geocodio.EnvOldAPIKey) - } - os.Setenv(geocodio.EnvGeocodioAPIKey, "") - os.Setenv(geocodio.EnvOldAPIKey, "") _, err := geocodio.New("") if err == nil { @@ -88,30 +77,3 @@ func TestGeocodioWithoutApiKeyEnvAndEmptyString(t *testing.T) { os.Setenv(geocodio.EnvGeocodioAPIKey, key) } - -func TestGeocodioDeprecatedWithApiKey(t *testing.T) { - _, err := geocodio.NewGeocodio(os.Getenv(geocodio.EnvGeocodioAPIKey)) - if err != nil { - t.Error("Failed with API KEY set.", err) - } -} - -func TestGeocodioDeprecatedWithoutApiKey(t *testing.T) { - _, err := geocodio.NewGeocodio("") - if err == nil { - t.Error("Expected error:", geocodio.ErrMissingAPIKey, " but did not see any error") - } -} - -func TestGeocodioWithOldApiKey(t *testing.T) { - os.Setenv(geocodio.EnvOldAPIKey, os.Getenv(geocodio.EnvGeocodioAPIKey)) - os.Setenv(geocodio.EnvGeocodioAPIKey, "") - - _, err := geocodio.New() - if err != nil { - t.Error("Failed with API KEY set.", err) - } - - os.Setenv(geocodio.EnvGeocodioAPIKey, os.Getenv(geocodio.EnvOldAPIKey)) - os.Setenv(geocodio.EnvOldAPIKey, "") -} diff --git a/go.mod b/go.mod index 4332f9e..8242654 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/stevepartridge/geocodio go 1.13 -require github.com/dghubble/sling v1.4.0 // indirect +require ( + github.com/dghubble/sling v1.4.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect +) diff --git a/go.sum b/go.sum index 7fcf87b..a5192de 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,16 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/reverse.go b/reverse.go index e712bcc..fef7130 100644 --- a/reverse.go +++ b/reverse.go @@ -19,17 +19,17 @@ func (g *Geocodio) Reverse(latitude, longitude float64) (GeocodeResult, error) { latStr := strconv.FormatFloat(latitude, 'f', 9, 64) lngStr := strconv.FormatFloat(longitude, 'f', 9, 64) - resp := GeocodeResult{} - err := g.get("/reverse", map[string]string{"q": latStr + "," + lngStr}, &resp) + res := GeocodeResult{} + err := g.do("GET", "/reverse", map[string]string{"q": latStr + "," + lngStr}, nil, &res) if err != nil { - return resp, err + return res, err } - if len(resp.Results) == 0 { - return resp, ErrNoResultsFound + if len(res.Results) == 0 { + return res, ErrNoResultsFound } - return resp, nil + return res, nil } // ReverseGeocode is deprecated and will be removed in 2+ @@ -46,13 +46,13 @@ func (g *Geocodio) ReverseGeocode(latitude, longitude float64) (GeocodeResult, e // ReverseBatch supports a batch lookup by lat/lng coordinate pairs func (g *Geocodio) ReverseBatch(latlngs ...float64) (BatchResponse, error) { - resp := BatchResponse{} + res := BatchResponse{} if len(latlngs) == 0 { - return resp, ErrReverseBatchMissingCoords + return res, ErrReverseBatchMissingCoords } if len(latlngs)%2 == 1 { - return resp, ErrReverseBatchInvalidCoordsPairs + return res, ErrReverseBatchInvalidCoordsPairs } var ( @@ -74,15 +74,15 @@ func (g *Geocodio) ReverseBatch(latlngs ...float64) (BatchResponse, error) { pair = coord } - err := g.post("/reverse", payload, nil, &resp) + err := g.do("POST", "/reverse", nil, payload, &res) if err != nil { - return resp, err + return res, err } - if len(resp.Results) == 0 { - return resp, ErrNoResultsFound + if len(res.Results) == 0 { + return res, ErrNoResultsFound } - return resp, nil + return res, nil } diff --git a/reverse_test.go b/reverse_test.go index b09d3af..88ebe19 100644 --- a/reverse_test.go +++ b/reverse_test.go @@ -1,7 +1,6 @@ package geocodio_test import ( - "fmt" "testing" "github.com/stevepartridge/geocodio" @@ -87,7 +86,6 @@ func TestReverseBatchLookup(t *testing.T) { AddressTestTwoLatitude, AddressTestTwoLongitude, AddressTestThreeLatitude, AddressTestThreeLongitude, ) - fmt.Println(result.ResponseAsString()) if err != nil { t.Error(err) } diff --git a/school.go b/school.go index aa62c62..49ac564 100644 --- a/school.go +++ b/school.go @@ -7,14 +7,6 @@ type SchoolDistricts struct { } // SchoolDistrict field -/* -{ - "name": "Desert Sands Unified School District", - "lea_code": "11110", - "grade_low": "KG", - "grade_high": "12" -} -*/ type SchoolDistrict struct { Name string `json:"name"` LEACode string `json:"lea_code"` diff --git a/timezone.go b/timezone.go index 6d47be5..6b9db4e 100644 --- a/timezone.go +++ b/timezone.go @@ -1,16 +1,6 @@ package geocodio // Timezone based on this payload -/* - "timezone": { - "name": "America/New_York", - "utc_offset": -5, - "observes_dst": true, - "abbreviation": "EST", - "source": "© OpenStreetMap contributors" - } -*/ - type Timezone struct { Name string `json:"name"` Abbreviation string `json:"abbreviation"` // v1.3+ diff --git a/zip.go b/zip.go index 5b71389..28df175 100644 --- a/zip.go +++ b/zip.go @@ -1,34 +1,6 @@ package geocodio // Zip4 based on this payload example -/* -{ - "record_type": { - "code": "S", - "description": "Street" - }, - "carrier_route": { - "id": "C007", - "description": "City Delivery" - }, - "building_or_firm_name": null, - "plus4": [ - "2890" - ], - "zip9": [ - "22201-2890" - ], - "government_building": null, - "facility_code": { - "code": "P", - "description": "Post Office" - }, - "city_delivery": true, - "valid_delivery_area": true, - "exact_match": true -} -*/ - type Zip4 struct { RecodeType RecordType `json:"record_type,omitempty"` CarrierRoute CarrierRoute `json:"carrier_route,omitempty"` From 7df2d34adf08aee1df76ac77800de55ba73874d1 Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Thu, 7 Oct 2021 13:58:47 -0700 Subject: [PATCH 4/6] consolidate --- address.go | 30 ------------------------------ env.go | 5 ----- fields.go | 13 ------------- geocodio.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ location.go | 6 ------ 5 files changed, 47 insertions(+), 54 deletions(-) delete mode 100644 address.go delete mode 100644 env.go delete mode 100644 fields.go delete mode 100644 location.go diff --git a/address.go b/address.go deleted file mode 100644 index 311ba1d..0000000 --- a/address.go +++ /dev/null @@ -1,30 +0,0 @@ -package geocodio - -type Address struct { - Query string `json:"query"` - Components Components `json:"address_components"` - Formatted string `json:"formatted_address"` - Location Location `json:"location"` - Accuracy float64 `json:"accuracy"` - AccuracyType string `json:"accuracy_type"` - Source string `json:"source"` - Fields Fields `json:"fields,omitempty"` -} - -// Components -type Components struct { - Number string `json:"number"` - Street string `json:"street"` - Suffix string `json:"suffix"` - SecondaryNumber string `json:"secondarynumber"` - SecondaryUnit string `json:"secondaryunit"` - PostDirectional string `json:"postdirectional"` - FormattedStreet string `json:"formatted_street"` - City string `json:"city"` - State string `json:"state"` - Zip string `json:"zip"` - County string `json:"county"` - Country string `json:"country"` - PreDirectional string `json:"predirectional"` - Prefix string `json:"prefix"` -} diff --git a/env.go b/env.go deleted file mode 100644 index 22c57fd..0000000 --- a/env.go +++ /dev/null @@ -1,5 +0,0 @@ -package geocodio - -const ( - EnvGeocodioAPIKey = "GEOCODIO_API_KEY" -) diff --git a/fields.go b/fields.go deleted file mode 100644 index c20aa37..0000000 --- a/fields.go +++ /dev/null @@ -1,13 +0,0 @@ -package geocodio - -// Fields -type Fields struct { - Timezone Timezone `json:"timezone,omitempty"` - Zip4 Zip4 `json:"zip4,omitempty"` - CongressionalDistrict CongressionalDistrict `json:"congressional_district,omitempty"` // v1.0 - CongressionalDistricts []CongressionalDistrict `json:"congressional_districts,omitempty"` // v1.1+ - StateLegislativeDistricts StateLegislativeDistricts `json:"state_legislative_districts,omitempty"` - SchoolDistricts SchoolDistricts `json:"school_districts,omitempty"` - Census CensusResults `json:"census,omitempty"` - ACS CensusACS `json:"acs,omitempty"` -} diff --git a/geocodio.go b/geocodio.go index 5c2ce15..f2d75ce 100644 --- a/geocodio.go +++ b/geocodio.go @@ -12,6 +12,7 @@ import ( const ( // GeocodioAPIBaseURLv1 is the Geocod.io Base URL GeocodioAPIBaseURLv1 = "https://api.geocod.io/v1.6" + EnvGeocodioAPIKey = "GEOCODIO_API_KEY" ) // Geocodio is the base struct @@ -25,6 +26,52 @@ type Input struct { FormattedAddress string `json:"formatted_address"` } +type Address struct { + Query string `json:"query"` + Components Components `json:"address_components"` + Formatted string `json:"formatted_address"` + Location Location `json:"location"` + Accuracy float64 `json:"accuracy"` + AccuracyType string `json:"accuracy_type"` + Source string `json:"source"` + Fields Fields `json:"fields,omitempty"` +} + +// Components +type Components struct { + Number string `json:"number"` + Street string `json:"street"` + Suffix string `json:"suffix"` + SecondaryNumber string `json:"secondarynumber"` + SecondaryUnit string `json:"secondaryunit"` + PostDirectional string `json:"postdirectional"` + FormattedStreet string `json:"formatted_street"` + City string `json:"city"` + State string `json:"state"` + Zip string `json:"zip"` + County string `json:"county"` + Country string `json:"country"` + PreDirectional string `json:"predirectional"` + Prefix string `json:"prefix"` +} + +type Location struct { + Latitude float64 `json:"lat"` + Longitude float64 `json:"lng"` +} + +// Fields +type Fields struct { + Timezone Timezone `json:"timezone,omitempty"` + Zip4 Zip4 `json:"zip4,omitempty"` + CongressionalDistrict CongressionalDistrict `json:"congressional_district,omitempty"` // v1.0 + CongressionalDistricts []CongressionalDistrict `json:"congressional_districts,omitempty"` // v1.1+ + StateLegislativeDistricts StateLegislativeDistricts `json:"state_legislative_districts,omitempty"` + SchoolDistricts SchoolDistricts `json:"school_districts,omitempty"` + Census CensusResults `json:"census,omitempty"` + ACS CensusACS `json:"acs,omitempty"` +} + // New creates a Geocodio instance based on an API key in either the environment // or passed in as the first string value func New(apiKey ...string) (*Geocodio, error) { diff --git a/location.go b/location.go deleted file mode 100644 index df564f6..0000000 --- a/location.go +++ /dev/null @@ -1,6 +0,0 @@ -package geocodio - -type Location struct { - Latitude float64 `json:"lat"` - Longitude float64 `json:"lng"` -} From e9c7062c4bbc9bfd9708dcc833c55a35e4169400 Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Mon, 11 Oct 2021 13:20:42 -0700 Subject: [PATCH 5/6] add Canada data fields, bug fixes. verify structs --- canada.go | 51 + census.go | 26 +- congressional.go | 4 +- geocode.go | 49 +- geocode_test.go | 17 +- geocodio.go | 13 +- geocodio_test.go | 21 + go.mod | 5 +- go.sum | 5 + school.go | 2 +- testdata/batchResponse.json | 106 ++ testdata/fields-acs-demographics.json | 409 +++++ testdata/fields-acs-economics.json | 117 ++ testdata/fields-acs-families.json | 222 +++ testdata/fields-acs-housing.json | 255 ++++ testdata/fields-acs-social.json | 476 ++++++ testdata/fields-cd.json | 124 ++ testdata/fields-census.json | 58 + testdata/fields-riding.json | 7 + testdata/fields-school (unified).json | 10 + testdata/fields-school.json | 16 + testdata/fields-statcan.json | 35 + testdata/fields-stateleg.json | 14 + testdata/fields-timezone.json | 9 + testdata/fields-zip4.json | 27 + testdata/singleResponse.json | 40 + testdata/singleResponseWithAllFields.json | 1692 +++++++++++++++++++++ zip.go | 2 +- 28 files changed, 3767 insertions(+), 45 deletions(-) create mode 100644 canada.go create mode 100644 testdata/batchResponse.json create mode 100644 testdata/fields-acs-demographics.json create mode 100644 testdata/fields-acs-economics.json create mode 100644 testdata/fields-acs-families.json create mode 100644 testdata/fields-acs-housing.json create mode 100644 testdata/fields-acs-social.json create mode 100644 testdata/fields-cd.json create mode 100644 testdata/fields-census.json create mode 100644 testdata/fields-riding.json create mode 100644 testdata/fields-school (unified).json create mode 100644 testdata/fields-school.json create mode 100644 testdata/fields-statcan.json create mode 100644 testdata/fields-stateleg.json create mode 100644 testdata/fields-timezone.json create mode 100644 testdata/fields-zip4.json create mode 100644 testdata/singleResponse.json create mode 100644 testdata/singleResponseWithAllFields.json diff --git a/canada.go b/canada.go new file mode 100644 index 0000000..df6c4b4 --- /dev/null +++ b/canada.go @@ -0,0 +1,51 @@ +package geocodio + +type Riding struct { + Code string `json:"code"` + NameFrench string `json:"name_french"` + NameEnglish string `json:"name_english"` +} + +type Statcan struct { + Division CanadaDivision `json:"division"` + ConsolidatedSubdivision CanadaConsolidatedSubdivision `json:"consolidated_subdivision"` + Subdivision CanadaSubdivision `json:"subdivision"` + EconomicRegion string `json:"economic_region"` + StatisticalArea CanadaStatisticalArea `json:"statistical_area"` + CensusMetroArea CanadaCensusMetroArea `json:"cma_ca"` + Tract string `json:"tract"` + CensusYear int `json:"census_year"` +} + +type CanadaDivision struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + TypeDescription string `json:"type_description"` +} + +type CanadaConsolidatedSubdivision struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type CanadaSubdivision struct { + ID string `json:"id"` + Name string `json:"name"` + TypeCode string `json:"type"` + TypeDescription string `json:"type_description"` +} + +type CanadaStatisticalArea struct { + Code string `json:"code"` + CodeDescription string `json:"code_description"` + Type string `json:"type"` + TypeDescription string `json:"type_description"` +} + +type CanadaCensusMetroArea struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + TypeDescription string `json:"type_description"` +} diff --git a/census.go b/census.go index 162a759..639d1ae 100644 --- a/census.go +++ b/census.go @@ -51,9 +51,9 @@ type CensusACS struct { } type Economics struct { - NumberOfHouseholds NumberOfHouseholds `json:"Number of households"` - MedianHouseholdIncome MedianHouseholdIncome `json:"Median household income"` - HouseholdIncome HouseholdIncome `json:"Household income"` + NumberOfHouseholds NumberOfHouseholds `json:"Number of households"` + MedianHouseholdIncome MedianHouseholdIncome `json:"Median household income"` + HouseholdIncome map[string]CensusDataPoint `json:"Household income"` // use map since Go tags cannot have "," in them } type NumberOfHouseholds struct { @@ -66,26 +66,6 @@ type MedianHouseholdIncome struct { Total CensusDataPoint `json:"Total"` } -type HouseholdIncome struct { - Meta CensusMeta `json:"meta"` - LessThan10000 CensusDataPoint `json:"Less than $10,000"` - Income10000to14999 CensusDataPoint `json:"$10,000 to $14,999"` - Income15000to19999 CensusDataPoint `json:"$15,000 to $19,999"` - Income20000to24999 CensusDataPoint `json:"$20,000 to $24,999"` - Income25000to29999 CensusDataPoint `json:"$25,000 to $29,999"` - Income30000to34999 CensusDataPoint `json:"$30,000 to $34,999"` - Income35000to39999 CensusDataPoint `json:"$35,000 to $39,999"` - Income40000to44999 CensusDataPoint `json:"$40,000 to $44,999"` - Income45000to49999 CensusDataPoint `json:"$45,000 to $49,999"` - Income50000to59000 CensusDataPoint `json:"$50,000 to $59,999"` - Income60000to74999 CensusDataPoint `json:"$60,000 to $74,999"` - Income75000to99999 CensusDataPoint `json:"$75,000 to $99,999"` - Income100000to124999 CensusDataPoint `json:"$100,000 to $124,999"` - Income125000to149000 CensusDataPoint `json:"$125,000 to $149,999"` - Income150000to199999 CensusDataPoint `json:"$150,000 to $199,999"` - Income200000orMore CensusDataPoint `json:"$200,000 or more"` -} - type Demographic struct { MedianAge map[string]CensusDataPoint `json:"Median age"` PopulationByAgeRange map[string]CensusDataPoint `json:"Population by age range"` diff --git a/congressional.go b/congressional.go index 8f72f58..cf0d0ba 100644 --- a/congressional.go +++ b/congressional.go @@ -6,7 +6,7 @@ type CongressionalDistrict struct { DistrictNumber int `json:"district_number"` CongressNumber string `json:"congress_number"` CongressYears string `json:"congress_years"` - Proportion int `json:"congress_years"` + Proportion int `json:"proportion"` CurrentLegislators []Legislator `json:"current_legislators"` // v1.2+ } @@ -68,5 +68,5 @@ type StateLegislativeDistricts struct { type StateLegislativeDistrict struct { Name string `json:"name"` - DistrictNumber string `json:"district_number"` + DistrictNumber int `json:"district_number"` } diff --git a/geocode.go b/geocode.go index f812e44..a6a008b 100644 --- a/geocode.go +++ b/geocode.go @@ -2,6 +2,7 @@ package geocodio import ( "errors" + "fmt" "strings" ) @@ -16,6 +17,7 @@ type BatchResult struct { Response BatchResultItem `json:"response"` } +// BatchResultItem type BatchResultItem struct { Input Input `json:"input,omitempty"` Results []Address `json:"results"` @@ -41,12 +43,31 @@ type Result struct { // Geocode single address // See: http://geocod.io/docs/#toc_4 func (g *Geocodio) Geocode(address string) (GeocodeResult, error) { - res := GeocodeResult{} if address == "" { - return res, ErrAddressIsEmpty + return GeocodeResult{}, ErrAddressIsEmpty } - err := g.do("GET", "/geocode", map[string]string{"q": address}, nil, &res) + return g.geocode(map[string]string{"q": address}) +} + +// Geocode single address with full component list +// See: https://www.geocod.io/docs/#single-address +func (g *Geocodio) GeocodeComponents(address InputAddress) (GeocodeResult, error) { + if address.Street == "" && address.City == "" && address.State == "" && address.PostalCode == "" && address.Country == "" { + return GeocodeResult{}, ErrAddressIsEmpty + } + + return g.geocode(map[string]string{ + "street": address.Street, + "city": address.City, + "state": address.State, + "postal_code": address.PostalCode, + "country": address.Country}) +} + +func (g *Geocodio) geocode(params map[string]string) (GeocodeResult, error) { + res := GeocodeResult{} + err := g.do("GET", "/geocode", params, nil, &res) if err != nil { return res, err } @@ -58,12 +79,15 @@ func (g *Geocodio) Geocode(address string) (GeocodeResult, error) { return res, nil } -// GeocodeBatch look up addresses -func (g *Geocodio) GeocodeBatch(addresses ...string) (BatchResponse, error) { +// GeocodeBatch lookup list of addresses (either string or InputAddress) +func (g *Geocodio) GeocodeBatch(addresses ...interface{}) (BatchResponse, error) { res := BatchResponse{} if len(addresses) == 0 { return res, ErrBatchAddressesIsEmpty } + if err := verifyValidAddresses(addresses); err != nil { + return res, err + } // TODO: support limit err := g.do("POST", "/geocode", nil, addresses, &res) @@ -78,6 +102,21 @@ func (g *Geocodio) GeocodeBatch(addresses ...string) (BatchResponse, error) { return res, nil } +func verifyValidAddresses(addresses []interface{}) error { + var builder strings.Builder + for i := range addresses { + switch addresses[i].(type) { + case string, InputAddress: + default: + builder.WriteString(fmt.Sprintf("address[%d]: %t ", i, addresses[i])) + } + } + if builder.Len() > 0 { + return fmt.Errorf("all addresses must be of type string or InputAddress: %s", builder.String()) + } + return nil +} + // GeocodeAndReturnTimezone will geocode and include Timezone in the fields response func (g *Geocodio) GeocodeAndReturnTimezone(address string) (GeocodeResult, error) { return g.GeocodeReturnFields(address, "timezone") diff --git a/geocode_test.go b/geocode_test.go index 0f2821d..316ac6b 100644 --- a/geocode_test.go +++ b/geocode_test.go @@ -81,7 +81,7 @@ func TestGeocodeFullAddressReturningCongressionalDistrict(t *testing.T) { } if len(result.Results[0].Fields.CongressionalDistricts) == 0 { - t.Error("Congressional District field not found", result.Results[0].Fields.CongressionalDistrict) + t.Error("Congressional District field not found", result.Results[0].Fields.CongressionalDistricts) t.Fail() } @@ -91,7 +91,7 @@ func TestGeocodeFullAddressReturningCongressionalDistrict(t *testing.T) { } if result.Results[0].Fields.CongressionalDistricts[0].DistrictNumber != 8 { - t.Error("Congressional District field does not match", result.Results[0].Fields.CongressionalDistrict) + t.Error("Congressional District field does not match", result.Results[0].Fields.CongressionalDistricts) t.Fail() } } @@ -125,12 +125,12 @@ func TestGeocodeFullAddressReturningStateLegislativeDistricts(t *testing.T) { t.Fail() } - if result.Results[0].Fields.StateLegislativeDistricts.House.DistrictNumber != "47" { + if result.Results[0].Fields.StateLegislativeDistricts.House.DistrictNumber != 47 { t.Error("State Legislative Districts house does not match", result.Results[0].Fields.StateLegislativeDistricts.House) t.Fail() } - if result.Results[0].Fields.StateLegislativeDistricts.Senate.DistrictNumber != "31" { + if result.Results[0].Fields.StateLegislativeDistricts.Senate.DistrictNumber != 31 { t.Error("State Legislative Districts senate does not match", result.Results[0].Fields.StateLegislativeDistricts.Senate) t.Fail() } @@ -170,19 +170,16 @@ func TestGeocodeFullAddressReturningMultipleFields(t *testing.T) { congressionalDistrict := geocodio.CongressionalDistrict{} - // check congressional district - if result.Results[0].Fields.CongressionalDistrict.Name != "" { - congressionalDistrict = result.Results[0].Fields.CongressionalDistrict - } else if len(result.Results[0].Fields.CongressionalDistricts) > 0 { + if len(result.Results[0].Fields.CongressionalDistricts) > 0 { congressionalDistrict = result.Results[0].Fields.CongressionalDistricts[0] } if congressionalDistrict.Name == "" { - t.Error("Congressional District field not found", congressionalDistrict) + t.Error("Congressional District field not found", congressionalDistrict.Name) } if congressionalDistrict.DistrictNumber != 8 { - t.Error("Congressional District field does not match", result.Results[0].Fields.CongressionalDistrict) + t.Error("Congressional District field does not match", congressionalDistrict.DistrictNumber) } } diff --git a/geocodio.go b/geocodio.go index f2d75ce..475717f 100644 --- a/geocodio.go +++ b/geocodio.go @@ -26,6 +26,16 @@ type Input struct { FormattedAddress string `json:"formatted_address"` } +// InputAddress contains the address components available +// for either a single address request or a batch request +type InputAddress struct { + Street string `json:"street"` + City string `json:"city"` + State string `json:"state"` + PostalCode string `json:"postal_code"` + Country string `json:"country"` +} + type Address struct { Query string `json:"query"` Components Components `json:"address_components"` @@ -64,12 +74,13 @@ type Location struct { type Fields struct { Timezone Timezone `json:"timezone,omitempty"` Zip4 Zip4 `json:"zip4,omitempty"` - CongressionalDistrict CongressionalDistrict `json:"congressional_district,omitempty"` // v1.0 CongressionalDistricts []CongressionalDistrict `json:"congressional_districts,omitempty"` // v1.1+ StateLegislativeDistricts StateLegislativeDistricts `json:"state_legislative_districts,omitempty"` SchoolDistricts SchoolDistricts `json:"school_districts,omitempty"` Census CensusResults `json:"census,omitempty"` ACS CensusACS `json:"acs,omitempty"` + Riding Riding `json:"riding,omitempty"` + Statcan Statcan `json:"statcan,omitempty"` } // New creates a Geocodio instance based on an API key in either the environment diff --git a/geocodio_test.go b/geocodio_test.go index 011c462..e7526a1 100644 --- a/geocodio_test.go +++ b/geocodio_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/mypricehealth/jsonassert" "github.com/stevepartridge/geocodio" ) @@ -77,3 +78,23 @@ func TestGeocodioWithoutApiKeyEnvAndEmptyString(t *testing.T) { os.Setenv(geocodio.EnvGeocodioAPIKey, key) } + +func TestVerifyStructs(t *testing.T) { + jsonassert.StructCheck(t, "testdata/batchResponse.json", &geocodio.BatchResponse{}) + jsonassert.StructCheck(t, "testdata/fields-acs-demographics.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-acs-economics.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-acs-families.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-acs-housing.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-acs-social.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-cd.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-census.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-riding.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-school (unified).json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-school.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-statcan.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-stateleg.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-timezone.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/fields-zip4.json", &geocodio.Fields{}) + jsonassert.StructCheck(t, "testdata/singleResponse.json", &geocodio.GeocodeResult{}) + jsonassert.StructCheck(t, "testdata/singleResponseWithAllFields.json", &geocodio.GeocodeResult{}) +} diff --git a/go.mod b/go.mod index 8242654..dee1902 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stevepartridge/geocodio go 1.13 require ( - github.com/dghubble/sling v1.4.0 // indirect - github.com/stretchr/testify v1.7.0 // indirect + github.com/dghubble/sling v1.4.0 + github.com/mypricehealth/jsonassert v0.0.0-20211011194146-84dc9e20f6e9 + github.com/stretchr/testify v1.7.0 ) diff --git a/go.sum b/go.sum index a5192de..1f6c3aa 100644 --- a/go.sum +++ b/go.sum @@ -2,15 +2,20 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dghubble/sling v1.4.0 h1:/n8MRosVTthvMbwlNZgLx579OGVjUOy3GNEv5BIqAWY= github.com/dghubble/sling v1.4.0/go.mod h1:0r40aNsU9EdDUVBNhfCstAtFgutjgJGYbO1oNzkMoM8= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/mypricehealth/jsonassert v0.0.0-20211011194146-84dc9e20f6e9 h1://4h8UKY5Sx/tXEUsEXJtX6fnUNLQ24bEUaBCQMnrIE= +github.com/mypricehealth/jsonassert v0.0.0-20211011194146-84dc9e20f6e9/go.mod h1:VRikT2/QYZ7/pDFDI6YIaLGaityo8z0+VKM/ahm+hls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/school.go b/school.go index 49ac564..8255e35 100644 --- a/school.go +++ b/school.go @@ -3,7 +3,7 @@ package geocodio type SchoolDistricts struct { Unified SchoolDistrict `json:"unified"` Elementary SchoolDistrict `json:"elementary"` - Secondar SchoolDistrict `json:"secondary"` + Secondary SchoolDistrict `json:"secondary"` } // SchoolDistrict field diff --git a/testdata/batchResponse.json b/testdata/batchResponse.json new file mode 100644 index 0000000..1833889 --- /dev/null +++ b/testdata/batchResponse.json @@ -0,0 +1,106 @@ +{ + "results": [ + { + "query": "1109 N Highland St, Arlington VA", + "response": { + "input": { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "state": "VA", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA" + }, + "results": [ + { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "county": "Arlington County", + "state": "VA", + "zip": "22201", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA 22201", + "location": { + "lat": 38.886672, + "lng": -77.094735 + }, + "accuracy": 1, + "accuracy_type": "rooftop", + "source": "Arlington" + }, + { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "county": "Arlington County", + "state": "VA", + "zip": "22201", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA 22201", + "location": { + "lat": 38.886665, + "lng": -77.094733 + }, + "accuracy": 1, + "accuracy_type": "rooftop", + "source": "Virginia Geographic Information Network (VGIN)" + } + ] + } + }, + { + "query": "525 University Ave, Toronto, ON, Canada", + "response": { + "input": { + "address_components": { + "number": "525", + "street": "University", + "suffix": "Ave", + "formatted_street": "University Ave", + "city": "Toronto", + "state": "ON", + "country": "CA" + }, + "formatted_address": "525 University Ave, Toronto, ON" + }, + "results": [ + { + "address_components": { + "number": "525", + "street": "University", + "suffix": "Ave", + "formatted_street": "University Ave", + "city": "Toronto", + "state": "ON", + "country": "CA" + }, + "formatted_address": "525 University Ave, Toronto, ON", + "location": { + "lat": 43.656258, + "lng": -79.388223 + }, + "accuracy": 1, + "accuracy_type": "rooftop", + "source": "City of Toronto Open Data" + } + ] + } + } + ] +} diff --git a/testdata/fields-acs-demographics.json b/testdata/fields-acs-demographics.json new file mode 100644 index 0000000..62ae16f --- /dev/null +++ b/testdata/fields-acs-demographics.json @@ -0,0 +1,409 @@ +{ + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "demographics": { + "Median age": { + "meta": { + "table_id": "B01002", + "universe": "Total population" + }, + "Total": { + "value": 32.6, + "margin_of_error": 0.6 + }, + "Male": { + "value": 33.8, + "margin_of_error": 1.8 + }, + "Female": { + "value": 32.1, + "margin_of_error": 0.7 + } + }, + "Population by age range": { + "meta": { + "table_id": "B01001", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Male": { + "value": 1835, + "margin_of_error": 195, + "percentage": 0.501 + }, + "Male: Under 5 years": { + "value": 101, + "margin_of_error": 66, + "percentage": 0.055 + }, + "Male: 5 to 9 years": { + "value": 32, + "margin_of_error": 27, + "percentage": 0.017 + }, + "Male: 10 to 14 years": { + "value": 21, + "margin_of_error": 28, + "percentage": 0.011 + }, + "Male: 15 to 17 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 18 and 19 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 20 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 21 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 22 to 24 years": { + "value": 174, + "margin_of_error": 77, + "percentage": 0.095 + }, + "Male: 25 to 29 years": { + "value": 302, + "margin_of_error": 109, + "percentage": 0.165 + }, + "Male: 30 to 34 years": { + "value": 354, + "margin_of_error": 94, + "percentage": 0.193 + }, + "Male: 35 to 39 years": { + "value": 209, + "margin_of_error": 81, + "percentage": 0.114 + }, + "Male: 40 to 44 years": { + "value": 205, + "margin_of_error": 81, + "percentage": 0.112 + }, + "Male: 45 to 49 years": { + "value": 48, + "margin_of_error": 31, + "percentage": 0.026 + }, + "Male: 50 to 54 years": { + "value": 162, + "margin_of_error": 86, + "percentage": 0.088 + }, + "Male: 55 to 59 years": { + "value": 120, + "margin_of_error": 80, + "percentage": 0.065 + }, + "Male: 60 and 61 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 62 to 64 years": { + "value": 8, + "margin_of_error": 12, + "percentage": 0.004 + }, + "Male: 65 and 66 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 67 to 69 years": { + "value": 54, + "margin_of_error": 35, + "percentage": 0.029 + }, + "Male: 70 to 74 years": { + "value": 31, + "margin_of_error": 29, + "percentage": 0.017 + }, + "Male: 75 to 79 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 80 to 84 years": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.008 + }, + "Male: 85 years and over": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female": { + "value": 1829, + "margin_of_error": 203, + "percentage": 0.499 + }, + "Female: Under 5 years": { + "value": 116, + "margin_of_error": 65, + "percentage": 0.063 + }, + "Female: 5 to 9 years": { + "value": 91, + "margin_of_error": 79, + "percentage": 0.05 + }, + "Female: 10 to 14 years": { + "value": 32, + "margin_of_error": 40, + "percentage": 0.017 + }, + "Female: 15 to 17 years": { + "value": 32, + "margin_of_error": 42, + "percentage": 0.017 + }, + "Female: 18 and 19 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 20 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 21 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 22 to 24 years": { + "value": 108, + "margin_of_error": 75, + "percentage": 0.059 + }, + "Female: 25 to 29 years": { + "value": 366, + "margin_of_error": 93, + "percentage": 0.2 + }, + "Female: 30 to 34 years": { + "value": 419, + "margin_of_error": 122, + "percentage": 0.229 + }, + "Female: 35 to 39 years": { + "value": 212, + "margin_of_error": 74, + "percentage": 0.116 + }, + "Female: 40 to 44 years": { + "value": 124, + "margin_of_error": 55, + "percentage": 0.068 + }, + "Female: 45 to 49 years": { + "value": 136, + "margin_of_error": 64, + "percentage": 0.074 + }, + "Female: 50 to 54 years": { + "value": 51, + "margin_of_error": 41, + "percentage": 0.028 + }, + "Female: 55 to 59 years": { + "value": 33, + "margin_of_error": 30, + "percentage": 0.018 + }, + "Female: 60 and 61 years": { + "value": 11, + "margin_of_error": 15, + "percentage": 0.006 + }, + "Female: 62 to 64 years": { + "value": 37, + "margin_of_error": 45, + "percentage": 0.02 + }, + "Female: 65 and 66 years": { + "value": 12, + "margin_of_error": 16, + "percentage": 0.007 + }, + "Female: 67 to 69 years": { + "value": 13, + "margin_of_error": 19, + "percentage": 0.007 + }, + "Female: 70 to 74 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 75 to 79 years": { + "value": 36, + "margin_of_error": 37, + "percentage": 0.02 + }, + "Female: 80 to 84 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 85 years and over": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + }, + "Sex": { + "meta": { + "table_id": "B01001", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Male": { + "value": 1835, + "margin_of_error": 195, + "percentage": 0.501 + }, + "Female": { + "value": 1829, + "margin_of_error": 203, + "percentage": 0.499 + } + }, + "Race and ethnicity": { + "meta": { + "table_id": "B03002", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Not Hispanic or Latino": { + "value": 3396, + "margin_of_error": 324, + "percentage": 0.927 + }, + "Not Hispanic or Latino: White alone": { + "value": 2556, + "margin_of_error": 330, + "percentage": 0.753 + }, + "Not Hispanic or Latino: Black or African American alone": { + "value": 93, + "margin_of_error": 71, + "percentage": 0.027 + }, + "Not Hispanic or Latino: American Indian and Alaska Native alone": { + "value": 21, + "margin_of_error": 26, + "percentage": 0.006 + }, + "Not Hispanic or Latino: Asian alone": { + "value": 573, + "margin_of_error": 175, + "percentage": 0.169 + }, + "Not Hispanic or Latino: Native Hawaiian and Other Pacific Islander alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Not Hispanic or Latino: Some other race alone": { + "value": 22, + "margin_of_error": 26, + "percentage": 0.006 + }, + "Not Hispanic or Latino: Two or more races": { + "value": 131, + "margin_of_error": 69, + "percentage": 0.039 + }, + "Not Hispanic or Latino: Two or more races: Two races including Some other race": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Not Hispanic or Latino: Two or more races: Two races excluding Some other race, and three or more races": { + "value": 131, + "margin_of_error": 69, + "percentage": 1 + }, + "Hispanic or Latino": { + "value": 268, + "margin_of_error": 128, + "percentage": 0.073 + }, + "Hispanic or Latino: White alone": { + "value": 244, + "margin_of_error": 120, + "percentage": 0.91 + }, + "Hispanic or Latino: Black or African American alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: American Indian and Alaska Native alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Asian alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Native Hawaiian and Other Pacific Islander alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Some other race alone": { + "value": 24, + "margin_of_error": 28, + "percentage": 0.09 + }, + "Hispanic or Latino: Two or more races": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Two or more races: Two races including Some other race": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Two or more races: Two races excluding Some other race, and three or more races": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + } + } +} diff --git a/testdata/fields-acs-economics.json b/testdata/fields-acs-economics.json new file mode 100644 index 0000000..c2f588e --- /dev/null +++ b/testdata/fields-acs-economics.json @@ -0,0 +1,117 @@ +{ + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "economics": { + "Number of households": { + "meta": { + "table_id": "B19001", + "universe": "Households" + }, + "Total": { + "value": 1999, + "margin_of_error": 87 + } + }, + "Median household income": { + "meta": { + "table_id": "B19013", + "universe": "Households" + }, + "Total": { + "value": 158656, + "margin_of_error": 9227 + } + }, + "Household income": { + "meta": { + "table_id": "B19001", + "universe": "Households" + }, + "Less than $10,000": { + "value": 24, + "margin_of_error": 26, + "percentage": 0.012 + }, + "$10,000 to $14,999": { + "value": 20, + "margin_of_error": 22, + "percentage": 0.01 + }, + "$15,000 to $19,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$20,000 to $24,999": { + "value": 18, + "margin_of_error": 26, + "percentage": 0.009 + }, + "$25,000 to $29,999": { + "value": 10, + "margin_of_error": 16, + "percentage": 0.005 + }, + "$30,000 to $34,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$35,000 to $39,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$40,000 to $44,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$45,000 to $49,999": { + "value": 49, + "margin_of_error": 55, + "percentage": 0.025 + }, + "$50,000 to $59,999": { + "value": 10, + "margin_of_error": 15, + "percentage": 0.005 + }, + "$60,000 to $74,999": { + "value": 87, + "margin_of_error": 49, + "percentage": 0.044 + }, + "$75,000 to $99,999": { + "value": 175, + "margin_of_error": 71, + "percentage": 0.088 + }, + "$100,000 to $124,999": { + "value": 275, + "margin_of_error": 100, + "percentage": 0.138 + }, + "$125,000 to $149,999": { + "value": 223, + "margin_of_error": 88, + "percentage": 0.112 + }, + "$150,000 to $199,999": { + "value": 461, + "margin_of_error": 111, + "percentage": 0.231 + }, + "$200,000 or more": { + "value": 647, + "margin_of_error": 142, + "percentage": 0.324 + } + } + } + } +} diff --git a/testdata/fields-acs-families.json b/testdata/fields-acs-families.json new file mode 100644 index 0000000..87fe519 --- /dev/null +++ b/testdata/fields-acs-families.json @@ -0,0 +1,222 @@ +{ + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "families": { + "Household type by household": { + "meta": { + "table_id": "B11001", + "universe": "Households" + }, + "Total": { + "value": 1999, + "margin_of_error": 87 + }, + "Family households": { + "value": 689, + "margin_of_error": 138, + "percentage": 0.345 + }, + "Family households: Married-couple family": { + "value": 585, + "margin_of_error": 116, + "percentage": 0.849 + }, + "Family households: Other family": { + "value": 104, + "margin_of_error": 78, + "percentage": 0.151 + }, + "Family households: Other family: Male householder, no spouse present": { + "value": 51, + "margin_of_error": 67, + "percentage": 0.49 + }, + "Family households: Other family: Female householder, no spouse present": { + "value": 53, + "margin_of_error": 33, + "percentage": 0.51 + }, + "Nonfamily households": { + "value": 1310, + "margin_of_error": 139, + "percentage": 0.655 + }, + "Nonfamily households: Householder living alone": { + "value": 836, + "margin_of_error": 134, + "percentage": 0.638 + }, + "Nonfamily households: Householder not living alone": { + "value": 474, + "margin_of_error": 109, + "percentage": 0.362 + } + }, + "Household type by population": { + "meta": { + "table_id": "B11002", + "universe": "Population in Households" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "In family households": { + "value": 1799, + "margin_of_error": 379, + "percentage": 0.491 + }, + "In family households: In married-couple family": { + "value": 1539, + "margin_of_error": 323, + "percentage": 0.855 + }, + "In family households: In married-couple family: Relatives": { + "value": 1539, + "margin_of_error": 323, + "percentage": 1 + }, + "In family households: In married-couple family: Nonrelatives": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "In family households: In male householder, no spouse present, family": { + "value": 112, + "margin_of_error": 144, + "percentage": 0.062 + }, + "In family households: In male householder, no spouse present, family: Relatives": { + "value": 110, + "margin_of_error": 144, + "percentage": 0.982 + }, + "In family households: In male householder, no spouse present, family: Nonrelatives": { + "value": 2, + "margin_of_error": 5, + "percentage": 0.018 + }, + "In family households: In female householder, no spouse present, family": { + "value": 148, + "margin_of_error": 95, + "percentage": 0.082 + }, + "In family households: In female householder, no spouse present, family: Relatives": { + "value": 138, + "margin_of_error": 86, + "percentage": 0.932 + }, + "In family households: In female householder, no spouse present, family: Nonrelatives": { + "value": 10, + "margin_of_error": 16, + "percentage": 0.068 + }, + "In nonfamily households": { + "value": 1865, + "margin_of_error": 234, + "percentage": 0.509 + } + }, + "Marital status": { + "meta": { + "table_id": "B12001", + "universe": "Population 15 Years And Older" + }, + "Male": { + "value": 1681, + "margin_of_error": 162, + "percentage": 0.514 + }, + "Male: Never married": { + "value": 911, + "margin_of_error": 166, + "percentage": 0.542 + }, + "Male: Now married": { + "value": 672, + "margin_of_error": 135, + "percentage": 0.4 + }, + "Male: Now married: Married, spouse present": { + "value": 588, + "margin_of_error": 113, + "percentage": 0.875 + }, + "Male: Now married: Married, spouse absent": { + "value": 84, + "margin_of_error": 73, + "percentage": 0.125 + }, + "Male: Now married: Married, spouse absent: Separated": { + "value": 7, + "margin_of_error": 12, + "percentage": 0.083 + }, + "Male: Now married: Married, spouse absent: Other": { + "value": 77, + "margin_of_error": 72, + "percentage": 0.917 + }, + "Male: Widowed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: Divorced": { + "value": 98, + "margin_of_error": 53, + "percentage": 0.058 + }, + "Female": { + "value": 1590, + "margin_of_error": 152, + "percentage": 0.486 + }, + "Female: Never married": { + "value": 827, + "margin_of_error": 129, + "percentage": 0.52 + }, + "Female: Now married": { + "value": 566, + "margin_of_error": 111, + "percentage": 0.356 + }, + "Female: Now married: Married, spouse present": { + "value": 553, + "margin_of_error": 109, + "percentage": 0.977 + }, + "Female: Now married: Married, spouse absent": { + "value": 13, + "margin_of_error": 19, + "percentage": 0.023 + }, + "Female: Now married: Married, spouse absent: Separated": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Now married: Married, spouse absent: Other": { + "value": 13, + "margin_of_error": 19, + "percentage": 1 + }, + "Female: Widowed": { + "value": 26, + "margin_of_error": 29, + "percentage": 0.016 + }, + "Female: Divorced": { + "value": 171, + "margin_of_error": 67, + "percentage": 0.108 + } + } + } + } +} diff --git a/testdata/fields-acs-housing.json b/testdata/fields-acs-housing.json new file mode 100644 index 0000000..a9e28d0 --- /dev/null +++ b/testdata/fields-acs-housing.json @@ -0,0 +1,255 @@ +{ + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "housing": { + "Number of housing units": { + "meta": { + "table_id": "B25002", + "universe": "Housing Units" + }, + "Total": { + "value": 2120, + "margin_of_error": 49 + } + }, + "Occupancy status": { + "meta": { + "table_id": "B25002", + "universe": "Housing Units" + }, + "Occupied": { + "value": 1999, + "margin_of_error": 87, + "percentage": 0.943 + }, + "Vacant": { + "value": 121, + "margin_of_error": 71, + "percentage": 0.057 + } + }, + "Ownership of occupied units": { + "meta": { + "table_id": "B25003", + "universe": "Occupied Housing Units" + }, + "Owner occupied": { + "value": 610, + "margin_of_error": 102, + "percentage": 0.305 + }, + "Renter occupied": { + "value": 1389, + "margin_of_error": 108, + "percentage": 0.695 + } + }, + "Units in structure": { + "meta": { + "table_id": "B25024", + "universe": "Housing Units" + }, + "1, detached unit": { + "value": 170, + "margin_of_error": 69, + "percentage": 0.08 + }, + "1, attached unit": { + "value": 90, + "margin_of_error": 31, + "percentage": 0.042 + }, + "2 units": { + "value": 27, + "margin_of_error": 43, + "percentage": 0.013 + }, + "3 or 4 units": { + "value": 46, + "margin_of_error": 56, + "percentage": 0.022 + }, + "5 to 9 units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "10 to 19 unit": { + "value": 15, + "margin_of_error": 24, + "percentage": 0.007 + }, + "20 to 49 units": { + "value": 98, + "margin_of_error": 77, + "percentage": 0.046 + }, + "50 or more units": { + "value": 1674, + "margin_of_error": 109, + "percentage": 0.79 + }, + "Mobile home units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Boat, RV, van, etc. units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + }, + "Median value of owner-occupied housing units": { + "meta": { + "table_id": "B25077", + "universe": "Owner-Occupied Housing Units" + }, + "Total": { + "value": 624500, + "margin_of_error": 67024 + } + }, + "Value of owner-occupied housing units": { + "meta": { + "table_id": "B25075", + "universe": "Owner-Occupied Housing Units" + }, + "Less than $10,000": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$10,000 to $14,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$15,000 to $19,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$20,000 to $24,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$25,000 to $29,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$30,000 to $34,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$35,000 to $39,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$40,000 to $49,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$50,000 to $59,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$60,000 to $69,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$70,000 to $79,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$80,000 to $89,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$90,000 to $99,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$100,000 to $124,999": { + "value": 5, + "margin_of_error": 7, + "percentage": 0.008 + }, + "$125,000 to $149,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$150,000 to $174,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$175,000 to $199,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$200,000 to $249,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$250,000 to $299,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$300,000 to $399,999": { + "value": 6, + "margin_of_error": 11, + "percentage": 0.01 + }, + "$400,000 to $499,999": { + "value": 174, + "margin_of_error": 94, + "percentage": 0.285 + }, + "$500,000 to $749,999": { + "value": 241, + "margin_of_error": 64, + "percentage": 0.395 + }, + "$750,000 to $999,999": { + "value": 93, + "margin_of_error": 51, + "percentage": 0.152 + }, + "$1,000,000 to $1,499,999": { + "value": 53, + "margin_of_error": 32, + "percentage": 0.087 + }, + "$1,500,000 to $1,999,999": { + "value": 38, + "margin_of_error": 23, + "percentage": 0.062 + }, + "$2,000,000 or more": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + } + } +} diff --git a/testdata/fields-acs-social.json b/testdata/fields-acs-social.json new file mode 100644 index 0000000..b35d552 --- /dev/null +++ b/testdata/fields-acs-social.json @@ -0,0 +1,476 @@ +{ + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "social": { + "Population by minimum level of education": { + "meta": { + "table_id": "B15002", + "universe": "Population 25 Years And Over" + }, + "Total": { + "value": 2957, + "margin_of_error": 214 + }, + "Male": { + "value": 1507, + "margin_of_error": 154, + "percentage": 0.51 + }, + "Male: No schooling completed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: Nursery to 4th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 5th and 6th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 7th and 8th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 9th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 10th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 11th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 12th grade, no diploma": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: High school graduate (includes equivalency)": { + "value": 48, + "margin_of_error": 36, + "percentage": 0.032 + }, + "Male: Some college, less than 1 year": { + "value": 17, + "margin_of_error": 18, + "percentage": 0.011 + }, + "Male: Some college, 1 or more years, no degree": { + "value": 46, + "margin_of_error": 34, + "percentage": 0.031 + }, + "Male: Associate's degree": { + "value": 36, + "margin_of_error": 28, + "percentage": 0.024 + }, + "Male: Bachelor's degree": { + "value": 540, + "margin_of_error": 163, + "percentage": 0.358 + }, + "Male: Master's degree": { + "value": 442, + "margin_of_error": 112, + "percentage": 0.293 + }, + "Male: Professional school degree": { + "value": 307, + "margin_of_error": 82, + "percentage": 0.204 + }, + "Male: Doctorate degree": { + "value": 71, + "margin_of_error": 40, + "percentage": 0.047 + }, + "Female": { + "value": 1450, + "margin_of_error": 152, + "percentage": 0.49 + }, + "Female: No schooling completed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Nursery to 4th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 5th and 6th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 7th and 8th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 9th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 10th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 11th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 12th grade, no diploma": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: High school graduate (includes equivalency)": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Some college, less than 1 year": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Some college, 1 or more years, no degree": { + "value": 43, + "margin_of_error": 37, + "percentage": 0.03 + }, + "Female: Associate's degree": { + "value": 38, + "margin_of_error": 54, + "percentage": 0.026 + }, + "Female: Bachelor's degree": { + "value": 619, + "margin_of_error": 142, + "percentage": 0.427 + }, + "Female: Master's degree": { + "value": 425, + "margin_of_error": 106, + "percentage": 0.293 + }, + "Female: Professional school degree": { + "value": 253, + "margin_of_error": 70, + "percentage": 0.174 + }, + "Female: Doctorate degree": { + "value": 72, + "margin_of_error": 44, + "percentage": 0.05 + } + }, + "Population with veteran status": { + "meta": { + "table_id": "B21001", + "universe": "Civilian Population 18 Years And Over" + }, + "Total": { + "value": 3219, + "margin_of_error": 226 + }, + "Veteran": { + "value": 199, + "margin_of_error": 106, + "percentage": 0.062 + }, + "Nonveteran": { + "value": 3020, + "margin_of_error": 233, + "percentage": 0.938 + }, + "Male": { + "value": 1675, + "margin_of_error": 164, + "percentage": 0.52 + }, + "Male: Veteran": { + "value": 167, + "margin_of_error": 98, + "percentage": 0.1 + }, + "Male: Nonveteran": { + "value": 1508, + "margin_of_error": 185, + "percentage": 0.9 + }, + "Male: 18 to 34 years": { + "value": 828, + "margin_of_error": 148, + "percentage": 0.494 + }, + "Male: 18 to 34 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 18 to 34 years: Nonveteran": { + "value": 828, + "margin_of_error": 148, + "percentage": 1 + }, + "Male: 35 to 54 years": { + "value": 620, + "margin_of_error": 129, + "percentage": 0.37 + }, + "Male: 35 to 54 years: Veteran": { + "value": 130, + "margin_of_error": 98, + "percentage": 0.21 + }, + "Male: 35 to 54 years: Nonveteran": { + "value": 490, + "margin_of_error": 91, + "percentage": 0.79 + }, + "Male: 55 to 64 years": { + "value": 128, + "margin_of_error": 78, + "percentage": 0.076 + }, + "Male: 55 to 64 years: Veteran": { + "value": 17, + "margin_of_error": 22, + "percentage": 0.133 + }, + "Male: 55 to 64 years: Nonveteran": { + "value": 111, + "margin_of_error": 72, + "percentage": 0.867 + }, + "Male: 65 to 74 years": { + "value": 85, + "margin_of_error": 54, + "percentage": 0.051 + }, + "Male: 65 to 74 years: Veteran": { + "value": 6, + "margin_of_error": 10, + "percentage": 0.071 + }, + "Male: 65 to 74 years: Nonveteran": { + "value": 79, + "margin_of_error": 52, + "percentage": 0.929 + }, + "Male: 75 years and over": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.008 + }, + "Male: 75 years and over: Veteran": { + "value": 14, + "margin_of_error": 23, + "percentage": 1 + }, + "Male: 75 years and over: Nonveteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female": { + "value": 1544, + "margin_of_error": 153, + "percentage": 0.48 + }, + "Female: Veteran": { + "value": 32, + "margin_of_error": 34, + "percentage": 0.021 + }, + "Female: Nonveteran": { + "value": 1512, + "margin_of_error": 156, + "percentage": 0.979 + }, + "Female: 18 to 34 years": { + "value": 891, + "margin_of_error": 136, + "percentage": 0.577 + }, + "Female: 18 to 34 years: Veteran": { + "value": 22, + "margin_of_error": 31, + "percentage": 0.025 + }, + "Female: 18 to 34 years: Nonveteran": { + "value": 869, + "margin_of_error": 137, + "percentage": 0.975 + }, + "Female: 35 to 54 years": { + "value": 511, + "margin_of_error": 115, + "percentage": 0.331 + }, + "Female: 35 to 54 years: Veteran": { + "value": 10, + "margin_of_error": 15, + "percentage": 0.02 + }, + "Female: 35 to 54 years: Nonveteran": { + "value": 501, + "margin_of_error": 112, + "percentage": 0.98 + }, + "Female: 55 to 64 years": { + "value": 81, + "margin_of_error": 57, + "percentage": 0.052 + }, + "Female: 55 to 64 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 55 to 64 years: Nonveteran": { + "value": 81, + "margin_of_error": 57, + "percentage": 1 + }, + "Female: 65 to 74 years": { + "value": 25, + "margin_of_error": 35, + "percentage": 0.016 + }, + "Female: 65 to 74 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 65 to 74 years: Nonveteran": { + "value": 25, + "margin_of_error": 35, + "percentage": 1 + }, + "Female: 75 years and over": { + "value": 36, + "margin_of_error": 37, + "percentage": 0.023 + }, + "Female: 75 years and over: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 75 years and over: Nonveteran": { + "value": 36, + "margin_of_error": 37, + "percentage": 1 + } + }, + "Period of military service for veterans": { + "meta": { + "table_id": "B21002", + "universe": "Civilian Veterans 18 Years And Over" + }, + "Total": { + "value": 199, + "margin_of_error": 106 + }, + "Gulf War (9/2001 or later), no Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 78, + "margin_of_error": 50, + "percentage": 0.392 + }, + "Gulf War (9/2001 or later) and Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 14, + "margin_of_error": 15, + "percentage": 0.07 + }, + "Gulf War (9/2001 or later), and Gulf War (8/1990 to 8/2001), and Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 9, + "margin_of_error": 14, + "percentage": 0.045 + }, + "Gulf War (8/1990 to 8/2001) and Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Vietnam Era, no Korean War, no World War II": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.07 + }, + "Vietnam Era and Korean War, no World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Vietnam Era and Korean War and World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Korean War, no Vietnam Era, no World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Korean War and World War II, no Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "World War II, no Korean War, no Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Between Gulf War and Vietnam Era only": { + "value": 78, + "margin_of_error": 82, + "percentage": 0.392 + }, + "Between Vietnam Era and Korean War only": { + "value": 6, + "margin_of_error": 10, + "percentage": 0.03 + }, + "Between Korean War and World War II only": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Pre-World War II only": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + } + } +} diff --git a/testdata/fields-cd.json b/testdata/fields-cd.json new file mode 100644 index 0000000..3d40ad0 --- /dev/null +++ b/testdata/fields-cd.json @@ -0,0 +1,124 @@ +{ + "congressional_districts": [ + { + "name": "Congressional District 8", + "district_number": 8, + "congress_number": "117th", + "congress_years": "2021-2023", + "proportion": 1, + "current_legislators": [ + { + "type": "representative", + "bio": { + "last_name": "Beyer", + "first_name": "Donald", + "birthday": "1950-06-20", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://beyer.house.gov", + "address": "1119 Longworth House Office Building Washington DC 20515-4608", + "phone": "202-225-4376", + "contact_form": null + }, + "social": { + "rss_url": null, + "twitter": "RepDonBeyer", + "facebook": "RepDonBeyer", + "youtube": null, + "youtube_id": "UCPJGVbOVcAVGiBwq8qr_T9w" + }, + "references": { + "bioguide_id": "B001292", + "thomas_id": "02272", + "opensecrets_id": "N00036018", + "lis_id": null, + "cspan_id": "21141", + "govtrack_id": "412657", + "votesmart_id": "1707", + "ballotpedia_id": "Don Beyer", + "washington_post_id": null, + "icpsr_id": "21554", + "wikipedia_id": "Don Beyer" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + }, + { + "type": "senator", + "bio": { + "last_name": "Warner", + "first_name": "Mark", + "birthday": "1954-12-15", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://www.warner.senate.gov", + "address": "703 Hart Senate Office Building Washington DC 20510", + "phone": "202-224-2023", + "contact_form": "http://www.warner.senate.gov/public/index.cfm?p=Contact" + }, + "social": { + "rss_url": "http://www.warner.senate.gov/public/?a=rss.feed", + "twitter": "MarkWarner", + "facebook": "MarkRWarner", + "youtube": "SenatorMarkWarner", + "youtube_id": "UCwyivNlEGf4sGd1oDLfY5jw" + }, + "references": { + "bioguide_id": "W000805", + "thomas_id": "01897", + "opensecrets_id": "N00002097", + "lis_id": "S327", + "cspan_id": "7630", + "govtrack_id": "412321", + "votesmart_id": "535", + "ballotpedia_id": "Mark Warner", + "washington_post_id": null, + "icpsr_id": "40909", + "wikipedia_id": "Mark Warner" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + }, + { + "type": "senator", + "bio": { + "last_name": "Kaine", + "first_name": "Timothy", + "birthday": "1958-02-26", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://www.kaine.senate.gov", + "address": "231 Russell Senate Office Building Washington DC 20510", + "phone": "202-224-4024", + "contact_form": "https://www.kaine.senate.gov/contact" + }, + "social": { + "rss_url": "http://www.kaine.senate.gov/rss/feeds/?type=all", + "twitter": null, + "facebook": "SenatorKaine", + "youtube": "SenatorTimKaine", + "youtube_id": "UC27LgTZlUnBQoNEQFZdn9LA" + }, + "references": { + "bioguide_id": "K000384", + "thomas_id": "02176", + "opensecrets_id": "N00033177", + "lis_id": "S362", + "cspan_id": "49219", + "govtrack_id": "412582", + "votesmart_id": "50772", + "ballotpedia_id": "Tim Kaine", + "washington_post_id": null, + "icpsr_id": "41305", + "wikipedia_id": "Tim Kaine" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + } + ] + } + ] +} diff --git a/testdata/fields-census.json b/testdata/fields-census.json new file mode 100644 index 0000000..83d4c76 --- /dev/null +++ b/testdata/fields-census.json @@ -0,0 +1,58 @@ +{ + "census": { + "2010": { + "census_year": 2010, + "state_fips": "51", + "county_fips": "51013", + "tract_code": "101801", + "block_code": "1004", + "block_group": "1", + "full_fips": "510131018011004", + "place": { + "name": "Arlington", + "fips": "5103000" + }, + "metro_micro_statistical_area": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47900", + "type": "metropolitan" + }, + "combined_statistical_area": { + "name": "Washington-Baltimore-Northern Virginia, DC-MD-VA-WV", + "area_code": "51548" + }, + "metropolitan_division": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47894" + }, + "source": "US Census Bureau" + }, + "2020": { + "census_year": 2020, + "state_fips": "51", + "county_fips": "51013", + "tract_code": "101801", + "block_code": "1004", + "block_group": "1", + "full_fips": "510131018011004", + "place": { + "name": "Arlington", + "fips": "5103000" + }, + "metro_micro_statistical_area": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47900", + "type": "metropolitan" + }, + "combined_statistical_area": { + "name": "Washington-Baltimore-Arlington, DC-MD-VA-WV-PA", + "area_code": "548" + }, + "metropolitan_division": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47894" + }, + "source": "US Census Bureau" + } + } +} diff --git a/testdata/fields-riding.json b/testdata/fields-riding.json new file mode 100644 index 0000000..e6c7786 --- /dev/null +++ b/testdata/fields-riding.json @@ -0,0 +1,7 @@ +{ + "riding": { + "code": "24068", + "name_french": "Saint-Laurent", + "name_english": "Saint-Laurent" + } +} diff --git a/testdata/fields-school (unified).json b/testdata/fields-school (unified).json new file mode 100644 index 0000000..8feb80a --- /dev/null +++ b/testdata/fields-school (unified).json @@ -0,0 +1,10 @@ +{ + "school_districts": { + "unified": { + "name": "Desert Sands Unified School District", + "lea_code": "11110", + "grade_low": "KG", + "grade_high": "12" + } + } +} diff --git a/testdata/fields-school.json b/testdata/fields-school.json new file mode 100644 index 0000000..e3779aa --- /dev/null +++ b/testdata/fields-school.json @@ -0,0 +1,16 @@ +{ + "school_districts": { + "elementary": { + "name": "Topsfield School District", + "lea_code": "11670", + "grade_low": "PK", + "grade_high": "06" + }, + "secondary": { + "name": "Masconomet School District", + "lea_code": "07410", + "grade_low": "07", + "grade_high": "12" + } + } +} diff --git a/testdata/fields-statcan.json b/testdata/fields-statcan.json new file mode 100644 index 0000000..e1e04ee --- /dev/null +++ b/testdata/fields-statcan.json @@ -0,0 +1,35 @@ +{ + "statcan": { + "division": { + "id": "2466", + "name": "Montréal", + "type": "TÉ", + "type_description": "Territoire équivalent" + }, + "consolidated_subdivision": { + "id": "2466023", + "name": "Montréal" + }, + "subdivision": { + "id": "2466023", + "name": "Montréal", + "type": "V", + "type_description": "Ville" + }, + "economic_region": "Montréal", + "statistical_area": { + "code": "462", + "code_description": "CMA or CA", + "type": "1", + "type_description": "Census subdivision within census metropolitan area" + }, + "cma_ca": { + "id": "462", + "name": "Montréal", + "type": "B", + "type_description": "Census metropolitan area (CMA)" + }, + "tract": "4620415.04", + "census_year": 2016 + } +} diff --git a/testdata/fields-stateleg.json b/testdata/fields-stateleg.json new file mode 100644 index 0000000..1e9aca7 --- /dev/null +++ b/testdata/fields-stateleg.json @@ -0,0 +1,14 @@ +{ + "state_legislative_districts": { + "house": { + "name": "Assembly District 42", + "district_number": 42, + "is_upcoming_state_legislative_district": false + }, + "senate": { + "name": "State Senate District 28", + "district_number": 28, + "is_upcoming_state_legislative_district": false + } + } +} diff --git a/testdata/fields-timezone.json b/testdata/fields-timezone.json new file mode 100644 index 0000000..bbdfa37 --- /dev/null +++ b/testdata/fields-timezone.json @@ -0,0 +1,9 @@ +{ + "timezone": { + "name": "America/New_York", + "utc_offset": -5, + "observes_dst": true, + "abbreviation": "EST", + "source": "© OpenStreetMap contributors" + } +} diff --git a/testdata/fields-zip4.json b/testdata/fields-zip4.json new file mode 100644 index 0000000..c1fd5ff --- /dev/null +++ b/testdata/fields-zip4.json @@ -0,0 +1,27 @@ +{ + "zip4": { + "record_type": { + "code": "S", + "description": "Street" + }, + "carrier_route": { + "id": "C007", + "description": "City Delivery" + }, + "building_or_firm_name": null, + "plus4": [ + "2890" + ], + "zip9": [ + "22201-2890" + ], + "government_building": null, + "facility_code": { + "code": "P", + "description": "Post Office" + }, + "city_delivery": true, + "valid_delivery_area": true, + "exact_match": true + } +} diff --git a/testdata/singleResponse.json b/testdata/singleResponse.json new file mode 100644 index 0000000..b0744ab --- /dev/null +++ b/testdata/singleResponse.json @@ -0,0 +1,40 @@ +{ + "input": { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "state": "VA", + "zip": "22201", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA 22201" + }, + "results": [ + { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "county": "Arlington County", + "state": "VA", + "zip": "22201", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA 22201", + "location": { + "lat": 38.886665, + "lng": -77.094733 + }, + "accuracy": 1, + "accuracy_type": "rooftop", + "source": "Virginia GIS Clearinghouse" + } + ] +} diff --git a/testdata/singleResponseWithAllFields.json b/testdata/singleResponseWithAllFields.json new file mode 100644 index 0000000..3f8348d --- /dev/null +++ b/testdata/singleResponseWithAllFields.json @@ -0,0 +1,1692 @@ +{ + "input": { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "state": "VA", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA" + }, + "results": [ + { + "address_components": { + "number": "1109", + "predirectional": "N", + "street": "Highland", + "suffix": "St", + "formatted_street": "N Highland St", + "city": "Arlington", + "county": "Arlington County", + "state": "VA", + "zip": "22201", + "country": "US" + }, + "formatted_address": "1109 N Highland St, Arlington, VA 22201", + "location": { + "lat": 38.886672, + "lng": -77.094735 + }, + "accuracy": 1, + "accuracy_type": "rooftop", + "source": "Arlington", + "fields": { + "acs": { + "meta": { + "source": "American Community Survey from the US Census Bureau", + "survey_years": "2015-2019", + "survey_duration_years": "5" + }, + "demographics": { + "Median age": { + "meta": { + "table_id": "B01002", + "universe": "Total population" + }, + "Total": { + "value": 32.6, + "margin_of_error": 0.6 + }, + "Male": { + "value": 33.8, + "margin_of_error": 1.8 + }, + "Female": { + "value": 32.1, + "margin_of_error": 0.7 + } + }, + "Population by age range": { + "meta": { + "table_id": "B01001", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Male": { + "value": 1835, + "margin_of_error": 195, + "percentage": 0.501 + }, + "Male: Under 5 years": { + "value": 101, + "margin_of_error": 66, + "percentage": 0.055 + }, + "Male: 5 to 9 years": { + "value": 32, + "margin_of_error": 27, + "percentage": 0.017 + }, + "Male: 10 to 14 years": { + "value": 21, + "margin_of_error": 28, + "percentage": 0.011 + }, + "Male: 15 to 17 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 18 and 19 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 20 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 21 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 22 to 24 years": { + "value": 174, + "margin_of_error": 77, + "percentage": 0.095 + }, + "Male: 25 to 29 years": { + "value": 302, + "margin_of_error": 109, + "percentage": 0.165 + }, + "Male: 30 to 34 years": { + "value": 354, + "margin_of_error": 94, + "percentage": 0.193 + }, + "Male: 35 to 39 years": { + "value": 209, + "margin_of_error": 81, + "percentage": 0.114 + }, + "Male: 40 to 44 years": { + "value": 205, + "margin_of_error": 81, + "percentage": 0.112 + }, + "Male: 45 to 49 years": { + "value": 48, + "margin_of_error": 31, + "percentage": 0.026 + }, + "Male: 50 to 54 years": { + "value": 162, + "margin_of_error": 86, + "percentage": 0.088 + }, + "Male: 55 to 59 years": { + "value": 120, + "margin_of_error": 80, + "percentage": 0.065 + }, + "Male: 60 and 61 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 62 to 64 years": { + "value": 8, + "margin_of_error": 12, + "percentage": 0.004 + }, + "Male: 65 and 66 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 67 to 69 years": { + "value": 54, + "margin_of_error": 35, + "percentage": 0.029 + }, + "Male: 70 to 74 years": { + "value": 31, + "margin_of_error": 29, + "percentage": 0.017 + }, + "Male: 75 to 79 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 80 to 84 years": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.008 + }, + "Male: 85 years and over": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female": { + "value": 1829, + "margin_of_error": 203, + "percentage": 0.499 + }, + "Female: Under 5 years": { + "value": 116, + "margin_of_error": 65, + "percentage": 0.063 + }, + "Female: 5 to 9 years": { + "value": 91, + "margin_of_error": 79, + "percentage": 0.05 + }, + "Female: 10 to 14 years": { + "value": 32, + "margin_of_error": 40, + "percentage": 0.017 + }, + "Female: 15 to 17 years": { + "value": 32, + "margin_of_error": 42, + "percentage": 0.017 + }, + "Female: 18 and 19 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 20 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 21 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 22 to 24 years": { + "value": 108, + "margin_of_error": 75, + "percentage": 0.059 + }, + "Female: 25 to 29 years": { + "value": 366, + "margin_of_error": 93, + "percentage": 0.2 + }, + "Female: 30 to 34 years": { + "value": 419, + "margin_of_error": 122, + "percentage": 0.229 + }, + "Female: 35 to 39 years": { + "value": 212, + "margin_of_error": 74, + "percentage": 0.116 + }, + "Female: 40 to 44 years": { + "value": 124, + "margin_of_error": 55, + "percentage": 0.068 + }, + "Female: 45 to 49 years": { + "value": 136, + "margin_of_error": 64, + "percentage": 0.074 + }, + "Female: 50 to 54 years": { + "value": 51, + "margin_of_error": 41, + "percentage": 0.028 + }, + "Female: 55 to 59 years": { + "value": 33, + "margin_of_error": 30, + "percentage": 0.018 + }, + "Female: 60 and 61 years": { + "value": 11, + "margin_of_error": 15, + "percentage": 0.006 + }, + "Female: 62 to 64 years": { + "value": 37, + "margin_of_error": 45, + "percentage": 0.02 + }, + "Female: 65 and 66 years": { + "value": 12, + "margin_of_error": 16, + "percentage": 0.007 + }, + "Female: 67 to 69 years": { + "value": 13, + "margin_of_error": 19, + "percentage": 0.007 + }, + "Female: 70 to 74 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 75 to 79 years": { + "value": 36, + "margin_of_error": 37, + "percentage": 0.02 + }, + "Female: 80 to 84 years": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 85 years and over": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + }, + "Sex": { + "meta": { + "table_id": "B01001", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Male": { + "value": 1835, + "margin_of_error": 195, + "percentage": 0.501 + }, + "Female": { + "value": 1829, + "margin_of_error": 203, + "percentage": 0.499 + } + }, + "Race and ethnicity": { + "meta": { + "table_id": "B03002", + "universe": "Total population" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "Not Hispanic or Latino": { + "value": 3396, + "margin_of_error": 324, + "percentage": 0.927 + }, + "Not Hispanic or Latino: White alone": { + "value": 2556, + "margin_of_error": 330, + "percentage": 0.753 + }, + "Not Hispanic or Latino: Black or African American alone": { + "value": 93, + "margin_of_error": 71, + "percentage": 0.027 + }, + "Not Hispanic or Latino: American Indian and Alaska Native alone": { + "value": 21, + "margin_of_error": 26, + "percentage": 0.006 + }, + "Not Hispanic or Latino: Asian alone": { + "value": 573, + "margin_of_error": 175, + "percentage": 0.169 + }, + "Not Hispanic or Latino: Native Hawaiian and Other Pacific Islander alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Not Hispanic or Latino: Some other race alone": { + "value": 22, + "margin_of_error": 26, + "percentage": 0.006 + }, + "Not Hispanic or Latino: Two or more races": { + "value": 131, + "margin_of_error": 69, + "percentage": 0.039 + }, + "Not Hispanic or Latino: Two or more races: Two races including Some other race": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Not Hispanic or Latino: Two or more races: Two races excluding Some other race, and three or more races": { + "value": 131, + "margin_of_error": 69, + "percentage": 1 + }, + "Hispanic or Latino": { + "value": 268, + "margin_of_error": 128, + "percentage": 0.073 + }, + "Hispanic or Latino: White alone": { + "value": 244, + "margin_of_error": 120, + "percentage": 0.91 + }, + "Hispanic or Latino: Black or African American alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: American Indian and Alaska Native alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Asian alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Native Hawaiian and Other Pacific Islander alone": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Some other race alone": { + "value": 24, + "margin_of_error": 28, + "percentage": 0.09 + }, + "Hispanic or Latino: Two or more races": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Two or more races: Two races including Some other race": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Hispanic or Latino: Two or more races: Two races excluding Some other race, and three or more races": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + }, + "economics": { + "Number of households": { + "meta": { + "table_id": "B19001", + "universe": "Households" + }, + "Total": { + "value": 1999, + "margin_of_error": 87 + } + }, + "Median household income": { + "meta": { + "table_id": "B19013", + "universe": "Households" + }, + "Total": { + "value": 158656, + "margin_of_error": 9227 + } + }, + "Household income": { + "meta": { + "table_id": "B19001", + "universe": "Households" + }, + "Less than $10,000": { + "value": 24, + "margin_of_error": 26, + "percentage": 0.012 + }, + "$10,000 to $14,999": { + "value": 20, + "margin_of_error": 22, + "percentage": 0.01 + }, + "$15,000 to $19,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$20,000 to $24,999": { + "value": 18, + "margin_of_error": 26, + "percentage": 0.009 + }, + "$25,000 to $29,999": { + "value": 10, + "margin_of_error": 16, + "percentage": 0.005 + }, + "$30,000 to $34,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$35,000 to $39,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$40,000 to $44,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$45,000 to $49,999": { + "value": 49, + "margin_of_error": 55, + "percentage": 0.025 + }, + "$50,000 to $59,999": { + "value": 10, + "margin_of_error": 15, + "percentage": 0.005 + }, + "$60,000 to $74,999": { + "value": 87, + "margin_of_error": 49, + "percentage": 0.044 + }, + "$75,000 to $99,999": { + "value": 175, + "margin_of_error": 71, + "percentage": 0.088 + }, + "$100,000 to $124,999": { + "value": 275, + "margin_of_error": 100, + "percentage": 0.138 + }, + "$125,000 to $149,999": { + "value": 223, + "margin_of_error": 88, + "percentage": 0.112 + }, + "$150,000 to $199,999": { + "value": 461, + "margin_of_error": 111, + "percentage": 0.231 + }, + "$200,000 or more": { + "value": 647, + "margin_of_error": 142, + "percentage": 0.324 + } + } + }, + "families": { + "Household type by household": { + "meta": { + "table_id": "B11001", + "universe": "Households" + }, + "Total": { + "value": 1999, + "margin_of_error": 87 + }, + "Family households": { + "value": 689, + "margin_of_error": 138, + "percentage": 0.345 + }, + "Family households: Married-couple family": { + "value": 585, + "margin_of_error": 116, + "percentage": 0.849 + }, + "Family households: Other family": { + "value": 104, + "margin_of_error": 78, + "percentage": 0.151 + }, + "Family households: Other family: Male householder, no spouse present": { + "value": 51, + "margin_of_error": 67, + "percentage": 0.49 + }, + "Family households: Other family: Female householder, no spouse present": { + "value": 53, + "margin_of_error": 33, + "percentage": 0.51 + }, + "Nonfamily households": { + "value": 1310, + "margin_of_error": 139, + "percentage": 0.655 + }, + "Nonfamily households: Householder living alone": { + "value": 836, + "margin_of_error": 134, + "percentage": 0.638 + }, + "Nonfamily households: Householder not living alone": { + "value": 474, + "margin_of_error": 109, + "percentage": 0.362 + } + }, + "Household type by population": { + "meta": { + "table_id": "B11002", + "universe": "Population in Households" + }, + "Total": { + "value": 3664, + "margin_of_error": 320 + }, + "In family households": { + "value": 1799, + "margin_of_error": 379, + "percentage": 0.491 + }, + "In family households: In married-couple family": { + "value": 1539, + "margin_of_error": 323, + "percentage": 0.855 + }, + "In family households: In married-couple family: Relatives": { + "value": 1539, + "margin_of_error": 323, + "percentage": 1 + }, + "In family households: In married-couple family: Nonrelatives": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "In family households: In male householder, no spouse present, family": { + "value": 112, + "margin_of_error": 144, + "percentage": 0.062 + }, + "In family households: In male householder, no spouse present, family: Relatives": { + "value": 110, + "margin_of_error": 144, + "percentage": 0.982 + }, + "In family households: In male householder, no spouse present, family: Nonrelatives": { + "value": 2, + "margin_of_error": 5, + "percentage": 0.018 + }, + "In family households: In female householder, no spouse present, family": { + "value": 148, + "margin_of_error": 95, + "percentage": 0.082 + }, + "In family households: In female householder, no spouse present, family: Relatives": { + "value": 138, + "margin_of_error": 86, + "percentage": 0.932 + }, + "In family households: In female householder, no spouse present, family: Nonrelatives": { + "value": 10, + "margin_of_error": 16, + "percentage": 0.068 + }, + "In nonfamily households": { + "value": 1865, + "margin_of_error": 234, + "percentage": 0.509 + } + }, + "Marital status": { + "meta": { + "table_id": "B12001", + "universe": "Population 15 Years And Older" + }, + "Male": { + "value": 1681, + "margin_of_error": 162, + "percentage": 0.514 + }, + "Male: Never married": { + "value": 911, + "margin_of_error": 166, + "percentage": 0.542 + }, + "Male: Now married": { + "value": 672, + "margin_of_error": 135, + "percentage": 0.4 + }, + "Male: Now married: Married, spouse present": { + "value": 588, + "margin_of_error": 113, + "percentage": 0.875 + }, + "Male: Now married: Married, spouse absent": { + "value": 84, + "margin_of_error": 73, + "percentage": 0.125 + }, + "Male: Now married: Married, spouse absent: Separated": { + "value": 7, + "margin_of_error": 12, + "percentage": 0.083 + }, + "Male: Now married: Married, spouse absent: Other": { + "value": 77, + "margin_of_error": 72, + "percentage": 0.917 + }, + "Male: Widowed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: Divorced": { + "value": 98, + "margin_of_error": 53, + "percentage": 0.058 + }, + "Female": { + "value": 1590, + "margin_of_error": 152, + "percentage": 0.486 + }, + "Female: Never married": { + "value": 827, + "margin_of_error": 129, + "percentage": 0.52 + }, + "Female: Now married": { + "value": 566, + "margin_of_error": 111, + "percentage": 0.356 + }, + "Female: Now married: Married, spouse present": { + "value": 553, + "margin_of_error": 109, + "percentage": 0.977 + }, + "Female: Now married: Married, spouse absent": { + "value": 13, + "margin_of_error": 19, + "percentage": 0.023 + }, + "Female: Now married: Married, spouse absent: Separated": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Now married: Married, spouse absent: Other": { + "value": 13, + "margin_of_error": 19, + "percentage": 1 + }, + "Female: Widowed": { + "value": 26, + "margin_of_error": 29, + "percentage": 0.016 + }, + "Female: Divorced": { + "value": 171, + "margin_of_error": 67, + "percentage": 0.108 + } + } + }, + "housing": { + "Number of housing units": { + "meta": { + "table_id": "B25002", + "universe": "Housing Units" + }, + "Total": { + "value": 2120, + "margin_of_error": 49 + } + }, + "Occupancy status": { + "meta": { + "table_id": "B25002", + "universe": "Housing Units" + }, + "Occupied": { + "value": 1999, + "margin_of_error": 87, + "percentage": 0.943 + }, + "Vacant": { + "value": 121, + "margin_of_error": 71, + "percentage": 0.057 + } + }, + "Ownership of occupied units": { + "meta": { + "table_id": "B25003", + "universe": "Occupied Housing Units" + }, + "Owner occupied": { + "value": 610, + "margin_of_error": 102, + "percentage": 0.305 + }, + "Renter occupied": { + "value": 1389, + "margin_of_error": 108, + "percentage": 0.695 + } + }, + "Units in structure": { + "meta": { + "table_id": "B25024", + "universe": "Housing Units" + }, + "1, detached unit": { + "value": 170, + "margin_of_error": 69, + "percentage": 0.08 + }, + "1, attached unit": { + "value": 90, + "margin_of_error": 31, + "percentage": 0.042 + }, + "2 units": { + "value": 27, + "margin_of_error": 43, + "percentage": 0.013 + }, + "3 or 4 units": { + "value": 46, + "margin_of_error": 56, + "percentage": 0.022 + }, + "5 to 9 units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "10 to 19 unit": { + "value": 15, + "margin_of_error": 24, + "percentage": 0.007 + }, + "20 to 49 units": { + "value": 98, + "margin_of_error": 77, + "percentage": 0.046 + }, + "50 or more units": { + "value": 1674, + "margin_of_error": 109, + "percentage": 0.79 + }, + "Mobile home units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Boat, RV, van, etc. units": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + }, + "Median value of owner-occupied housing units": { + "meta": { + "table_id": "B25077", + "universe": "Owner-Occupied Housing Units" + }, + "Total": { + "value": 624500, + "margin_of_error": 67024 + } + }, + "Value of owner-occupied housing units": { + "meta": { + "table_id": "B25075", + "universe": "Owner-Occupied Housing Units" + }, + "Less than $10,000": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$10,000 to $14,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$15,000 to $19,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$20,000 to $24,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$25,000 to $29,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$30,000 to $34,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$35,000 to $39,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$40,000 to $49,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$50,000 to $59,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$60,000 to $69,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$70,000 to $79,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$80,000 to $89,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$90,000 to $99,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$100,000 to $124,999": { + "value": 5, + "margin_of_error": 7, + "percentage": 0.008 + }, + "$125,000 to $149,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$150,000 to $174,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$175,000 to $199,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$200,000 to $249,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$250,000 to $299,999": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "$300,000 to $399,999": { + "value": 6, + "margin_of_error": 11, + "percentage": 0.01 + }, + "$400,000 to $499,999": { + "value": 174, + "margin_of_error": 94, + "percentage": 0.285 + }, + "$500,000 to $749,999": { + "value": 241, + "margin_of_error": 64, + "percentage": 0.395 + }, + "$750,000 to $999,999": { + "value": 93, + "margin_of_error": 51, + "percentage": 0.152 + }, + "$1,000,000 to $1,499,999": { + "value": 53, + "margin_of_error": 32, + "percentage": 0.087 + }, + "$1,500,000 to $1,999,999": { + "value": 38, + "margin_of_error": 23, + "percentage": 0.062 + }, + "$2,000,000 or more": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + }, + "social": { + "Population by minimum level of education": { + "meta": { + "table_id": "B15002", + "universe": "Population 25 Years And Over" + }, + "Total": { + "value": 2957, + "margin_of_error": 214 + }, + "Male": { + "value": 1507, + "margin_of_error": 154, + "percentage": 0.51 + }, + "Male: No schooling completed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: Nursery to 4th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 5th and 6th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 7th and 8th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 9th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 10th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 11th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 12th grade, no diploma": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: High school graduate (includes equivalency)": { + "value": 48, + "margin_of_error": 36, + "percentage": 0.032 + }, + "Male: Some college, less than 1 year": { + "value": 17, + "margin_of_error": 18, + "percentage": 0.011 + }, + "Male: Some college, 1 or more years, no degree": { + "value": 46, + "margin_of_error": 34, + "percentage": 0.031 + }, + "Male: Associate's degree": { + "value": 36, + "margin_of_error": 28, + "percentage": 0.024 + }, + "Male: Bachelor's degree": { + "value": 540, + "margin_of_error": 163, + "percentage": 0.358 + }, + "Male: Master's degree": { + "value": 442, + "margin_of_error": 112, + "percentage": 0.293 + }, + "Male: Professional school degree": { + "value": 307, + "margin_of_error": 82, + "percentage": 0.204 + }, + "Male: Doctorate degree": { + "value": 71, + "margin_of_error": 40, + "percentage": 0.047 + }, + "Female": { + "value": 1450, + "margin_of_error": 152, + "percentage": 0.49 + }, + "Female: No schooling completed": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Nursery to 4th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 5th and 6th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 7th and 8th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 9th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 10th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 11th grade": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 12th grade, no diploma": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: High school graduate (includes equivalency)": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Some college, less than 1 year": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: Some college, 1 or more years, no degree": { + "value": 43, + "margin_of_error": 37, + "percentage": 0.03 + }, + "Female: Associate's degree": { + "value": 38, + "margin_of_error": 54, + "percentage": 0.026 + }, + "Female: Bachelor's degree": { + "value": 619, + "margin_of_error": 142, + "percentage": 0.427 + }, + "Female: Master's degree": { + "value": 425, + "margin_of_error": 106, + "percentage": 0.293 + }, + "Female: Professional school degree": { + "value": 253, + "margin_of_error": 70, + "percentage": 0.174 + }, + "Female: Doctorate degree": { + "value": 72, + "margin_of_error": 44, + "percentage": 0.05 + } + }, + "Population with veteran status": { + "meta": { + "table_id": "B21001", + "universe": "Civilian Population 18 Years And Over" + }, + "Total": { + "value": 3219, + "margin_of_error": 226 + }, + "Veteran": { + "value": 199, + "margin_of_error": 106, + "percentage": 0.062 + }, + "Nonveteran": { + "value": 3020, + "margin_of_error": 233, + "percentage": 0.938 + }, + "Male": { + "value": 1675, + "margin_of_error": 164, + "percentage": 0.52 + }, + "Male: Veteran": { + "value": 167, + "margin_of_error": 98, + "percentage": 0.1 + }, + "Male: Nonveteran": { + "value": 1508, + "margin_of_error": 185, + "percentage": 0.9 + }, + "Male: 18 to 34 years": { + "value": 828, + "margin_of_error": 148, + "percentage": 0.494 + }, + "Male: 18 to 34 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Male: 18 to 34 years: Nonveteran": { + "value": 828, + "margin_of_error": 148, + "percentage": 1 + }, + "Male: 35 to 54 years": { + "value": 620, + "margin_of_error": 129, + "percentage": 0.37 + }, + "Male: 35 to 54 years: Veteran": { + "value": 130, + "margin_of_error": 98, + "percentage": 0.21 + }, + "Male: 35 to 54 years: Nonveteran": { + "value": 490, + "margin_of_error": 91, + "percentage": 0.79 + }, + "Male: 55 to 64 years": { + "value": 128, + "margin_of_error": 78, + "percentage": 0.076 + }, + "Male: 55 to 64 years: Veteran": { + "value": 17, + "margin_of_error": 22, + "percentage": 0.133 + }, + "Male: 55 to 64 years: Nonveteran": { + "value": 111, + "margin_of_error": 72, + "percentage": 0.867 + }, + "Male: 65 to 74 years": { + "value": 85, + "margin_of_error": 54, + "percentage": 0.051 + }, + "Male: 65 to 74 years: Veteran": { + "value": 6, + "margin_of_error": 10, + "percentage": 0.071 + }, + "Male: 65 to 74 years: Nonveteran": { + "value": 79, + "margin_of_error": 52, + "percentage": 0.929 + }, + "Male: 75 years and over": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.008 + }, + "Male: 75 years and over: Veteran": { + "value": 14, + "margin_of_error": 23, + "percentage": 1 + }, + "Male: 75 years and over: Nonveteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female": { + "value": 1544, + "margin_of_error": 153, + "percentage": 0.48 + }, + "Female: Veteran": { + "value": 32, + "margin_of_error": 34, + "percentage": 0.021 + }, + "Female: Nonveteran": { + "value": 1512, + "margin_of_error": 156, + "percentage": 0.979 + }, + "Female: 18 to 34 years": { + "value": 891, + "margin_of_error": 136, + "percentage": 0.577 + }, + "Female: 18 to 34 years: Veteran": { + "value": 22, + "margin_of_error": 31, + "percentage": 0.025 + }, + "Female: 18 to 34 years: Nonveteran": { + "value": 869, + "margin_of_error": 137, + "percentage": 0.975 + }, + "Female: 35 to 54 years": { + "value": 511, + "margin_of_error": 115, + "percentage": 0.331 + }, + "Female: 35 to 54 years: Veteran": { + "value": 10, + "margin_of_error": 15, + "percentage": 0.02 + }, + "Female: 35 to 54 years: Nonveteran": { + "value": 501, + "margin_of_error": 112, + "percentage": 0.98 + }, + "Female: 55 to 64 years": { + "value": 81, + "margin_of_error": 57, + "percentage": 0.052 + }, + "Female: 55 to 64 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 55 to 64 years: Nonveteran": { + "value": 81, + "margin_of_error": 57, + "percentage": 1 + }, + "Female: 65 to 74 years": { + "value": 25, + "margin_of_error": 35, + "percentage": 0.016 + }, + "Female: 65 to 74 years: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 65 to 74 years: Nonveteran": { + "value": 25, + "margin_of_error": 35, + "percentage": 1 + }, + "Female: 75 years and over": { + "value": 36, + "margin_of_error": 37, + "percentage": 0.023 + }, + "Female: 75 years and over: Veteran": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Female: 75 years and over: Nonveteran": { + "value": 36, + "margin_of_error": 37, + "percentage": 1 + } + }, + "Period of military service for veterans": { + "meta": { + "table_id": "B21002", + "universe": "Civilian Veterans 18 Years And Over" + }, + "Total": { + "value": 199, + "margin_of_error": 106 + }, + "Gulf War (9/2001 or later), no Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 78, + "margin_of_error": 50, + "percentage": 0.392 + }, + "Gulf War (9/2001 or later) and Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 14, + "margin_of_error": 15, + "percentage": 0.07 + }, + "Gulf War (9/2001 or later), and Gulf War (8/1990 to 8/2001), and Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Gulf War (8/1990 to 8/2001), no Vietnam Era": { + "value": 9, + "margin_of_error": 14, + "percentage": 0.045 + }, + "Gulf War (8/1990 to 8/2001) and Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Vietnam Era, no Korean War, no World War II": { + "value": 14, + "margin_of_error": 23, + "percentage": 0.07 + }, + "Vietnam Era and Korean War, no World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Vietnam Era and Korean War and World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Korean War, no Vietnam Era, no World War II": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Korean War and World War II, no Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "World War II, no Korean War, no Vietnam Era": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Between Gulf War and Vietnam Era only": { + "value": 78, + "margin_of_error": 82, + "percentage": 0.392 + }, + "Between Vietnam Era and Korean War only": { + "value": 6, + "margin_of_error": 10, + "percentage": 0.03 + }, + "Between Korean War and World War II only": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + }, + "Pre-World War II only": { + "value": 0, + "margin_of_error": 12, + "percentage": 0 + } + } + } + }, + "census": { + "2010": { + "census_year": 2010, + "state_fips": "51", + "county_fips": "51013", + "tract_code": "101801", + "block_code": "1004", + "block_group": "1", + "full_fips": "510131018011004", + "place": { + "name": "Arlington", + "fips": "5103000" + }, + "metro_micro_statistical_area": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47900", + "type": "metropolitan" + }, + "combined_statistical_area": { + "name": "Washington-Baltimore-Northern Virginia, DC-MD-VA-WV", + "area_code": "51548" + }, + "metropolitan_division": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47894" + }, + "source": "US Census Bureau" + }, + "2020": { + "census_year": 2020, + "state_fips": "51", + "county_fips": "51013", + "tract_code": "101801", + "block_code": "1004", + "block_group": "1", + "full_fips": "510131018011004", + "place": { + "name": "Arlington", + "fips": "5103000" + }, + "metro_micro_statistical_area": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47900", + "type": "metropolitan" + }, + "combined_statistical_area": { + "name": "Washington-Baltimore-Arlington, DC-MD-VA-WV-PA", + "area_code": "548" + }, + "metropolitan_division": { + "name": "Washington-Arlington-Alexandria, DC-VA-MD-WV", + "area_code": "47894" + }, + "source": "US Census Bureau" + } + }, + "congressional_districts": [ + { + "name": "Congressional District 8", + "district_number": 8, + "congress_number": "117th", + "congress_years": "2021-2023", + "proportion": 1, + "current_legislators": [ + { + "type": "representative", + "bio": { + "last_name": "Beyer", + "first_name": "Donald", + "birthday": "1950-06-20", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://beyer.house.gov", + "address": "1119 Longworth House Office Building Washington DC 20515-4608", + "phone": "202-225-4376", + "contact_form": null + }, + "social": { + "rss_url": null, + "twitter": "RepDonBeyer", + "facebook": "RepDonBeyer", + "youtube": null, + "youtube_id": "UCPJGVbOVcAVGiBwq8qr_T9w" + }, + "references": { + "bioguide_id": "B001292", + "thomas_id": "02272", + "opensecrets_id": "N00036018", + "lis_id": null, + "cspan_id": "21141", + "govtrack_id": "412657", + "votesmart_id": "1707", + "ballotpedia_id": "Don Beyer", + "washington_post_id": null, + "icpsr_id": "21554", + "wikipedia_id": "Don Beyer" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + }, + { + "type": "senator", + "bio": { + "last_name": "Warner", + "first_name": "Mark", + "birthday": "1954-12-15", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://www.warner.senate.gov", + "address": "703 Hart Senate Office Building Washington DC 20510", + "phone": "202-224-2023", + "contact_form": "http://www.warner.senate.gov/public/index.cfm?p=Contact" + }, + "social": { + "rss_url": "http://www.warner.senate.gov/public/?a=rss.feed", + "twitter": "MarkWarner", + "facebook": "MarkRWarner", + "youtube": "SenatorMarkWarner", + "youtube_id": "UCwyivNlEGf4sGd1oDLfY5jw" + }, + "references": { + "bioguide_id": "W000805", + "thomas_id": "01897", + "opensecrets_id": "N00002097", + "lis_id": "S327", + "cspan_id": "7630", + "govtrack_id": "412321", + "votesmart_id": "535", + "ballotpedia_id": "Mark Warner", + "washington_post_id": null, + "icpsr_id": "40909", + "wikipedia_id": "Mark Warner" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + }, + { + "type": "senator", + "bio": { + "last_name": "Kaine", + "first_name": "Timothy", + "birthday": "1958-02-26", + "gender": "M", + "party": "Democrat" + }, + "contact": { + "url": "https://www.kaine.senate.gov", + "address": "231 Russell Senate Office Building Washington DC 20510", + "phone": "202-224-4024", + "contact_form": "https://www.kaine.senate.gov/contact" + }, + "social": { + "rss_url": "http://www.kaine.senate.gov/rss/feeds/?type=all", + "twitter": null, + "facebook": "SenatorKaine", + "youtube": "SenatorTimKaine", + "youtube_id": "UC27LgTZlUnBQoNEQFZdn9LA" + }, + "references": { + "bioguide_id": "K000384", + "thomas_id": "02176", + "opensecrets_id": "N00033177", + "lis_id": "S362", + "cspan_id": "49219", + "govtrack_id": "412582", + "votesmart_id": "50772", + "ballotpedia_id": "Tim Kaine", + "washington_post_id": null, + "icpsr_id": "41305", + "wikipedia_id": "Tim Kaine" + }, + "source": "Legislator data is originally collected and aggregated by https://github.com/unitedstates/" + } + ] + } + ], + "riding": { + "code": "24068", + "name_french": "Saint-Laurent", + "name_english": "Saint-Laurent" + }, + "school_districts": { + "unified": { + "name": "Desert Sands Unified School District", + "lea_code": "11110", + "grade_low": "KG", + "grade_high": "12" + } + }, + "state_legislative_districts": { + "house": { + "name": "State House District 47", + "district_number": 47, + "is_upcoming_state_legislative_district": false + }, + "senate": { + "name": "State Senate District 31", + "district_number": 31, + "is_upcoming_state_legislative_district": false + } + }, + "timezone": { + "name": "America/New_York", + "utc_offset": -5, + "observes_dst": true, + "abbreviation": "EST", + "source": "© OpenStreetMap contributors" + } + } + } + ] +} diff --git a/zip.go b/zip.go index 28df175..c6418a8 100644 --- a/zip.go +++ b/zip.go @@ -2,7 +2,7 @@ package geocodio // Zip4 based on this payload example type Zip4 struct { - RecodeType RecordType `json:"record_type,omitempty"` + RecordType RecordType `json:"record_type,omitempty"` CarrierRoute CarrierRoute `json:"carrier_route,omitempty"` BuildingOrFirmName string `json:"building_or_firm_name,omitempty"` Plus4 []string `json:"plus4,omitempty"` From 7894f9479be8c3dc4e13be8e02f2d150308011a6 Mon Sep 17 00:00:00 2001 From: Rob Archibald Date: Mon, 11 Oct 2021 14:09:33 -0700 Subject: [PATCH 6/6] match previous naming --- geocode.go | 42 +++++++++++++++++++++--------------------- reverse.go | 28 ++++++++++++++-------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/geocode.go b/geocode.go index a6a008b..d966bc1 100644 --- a/geocode.go +++ b/geocode.go @@ -66,40 +66,40 @@ func (g *Geocodio) GeocodeComponents(address InputAddress) (GeocodeResult, error } func (g *Geocodio) geocode(params map[string]string) (GeocodeResult, error) { - res := GeocodeResult{} - err := g.do("GET", "/geocode", params, nil, &res) + resp := GeocodeResult{} + err := g.do("GET", "/geocode", params, nil, &resp) if err != nil { - return res, err + return resp, err } - if len(res.Results) == 0 { - return res, ErrNoResultsFound + if len(resp.Results) == 0 { + return resp, ErrNoResultsFound } - return res, nil + return resp, nil } // GeocodeBatch lookup list of addresses (either string or InputAddress) func (g *Geocodio) GeocodeBatch(addresses ...interface{}) (BatchResponse, error) { - res := BatchResponse{} + resp := BatchResponse{} if len(addresses) == 0 { - return res, ErrBatchAddressesIsEmpty + return resp, ErrBatchAddressesIsEmpty } if err := verifyValidAddresses(addresses); err != nil { - return res, err + return resp, err } // TODO: support limit - err := g.do("POST", "/geocode", nil, addresses, &res) + err := g.do("POST", "/geocode", nil, addresses, &resp) if err != nil { - return res, err + return resp, err } - if len(res.Results) == 0 { - return res, ErrNoResultsFound + if len(resp.Results) == 0 { + return resp, ErrNoResultsFound } - return res, nil + return resp, nil } func verifyValidAddresses(addresses []interface{}) error { @@ -146,9 +146,9 @@ func (g *Geocodio) GeocodeAndReturnStateLegislativeDistricts(address string) (Ge Each field counts as an additional lookup each */ func (g *Geocodio) GeocodeReturnFields(address string, fields ...string) (GeocodeResult, error) { - res := GeocodeResult{} + resp := GeocodeResult{} if address == "" { - return res, errors.New("address can not be empty") + return resp, errors.New("address can not be empty") } fieldsCommaSeparated := strings.Join(fields, ",") @@ -156,14 +156,14 @@ func (g *Geocodio) GeocodeReturnFields(address string, fields ...string) (Geocod err := g.do("GET", "/geocode", map[string]string{ "q": address, "fields": fieldsCommaSeparated, - }, nil, &res) + }, nil, &resp) if err != nil { - return res, err + return resp, err } - if len(res.Results) == 0 { - return res, ErrNoResultsFound + if len(resp.Results) == 0 { + return resp, ErrNoResultsFound } - return res, nil + return resp, nil } diff --git a/reverse.go b/reverse.go index fef7130..9fc1eb0 100644 --- a/reverse.go +++ b/reverse.go @@ -19,17 +19,17 @@ func (g *Geocodio) Reverse(latitude, longitude float64) (GeocodeResult, error) { latStr := strconv.FormatFloat(latitude, 'f', 9, 64) lngStr := strconv.FormatFloat(longitude, 'f', 9, 64) - res := GeocodeResult{} - err := g.do("GET", "/reverse", map[string]string{"q": latStr + "," + lngStr}, nil, &res) + resp := GeocodeResult{} + err := g.do("GET", "/reverse", map[string]string{"q": latStr + "," + lngStr}, nil, &resp) if err != nil { - return res, err + return resp, err } - if len(res.Results) == 0 { - return res, ErrNoResultsFound + if len(resp.Results) == 0 { + return resp, ErrNoResultsFound } - return res, nil + return resp, nil } // ReverseGeocode is deprecated and will be removed in 2+ @@ -46,13 +46,13 @@ func (g *Geocodio) ReverseGeocode(latitude, longitude float64) (GeocodeResult, e // ReverseBatch supports a batch lookup by lat/lng coordinate pairs func (g *Geocodio) ReverseBatch(latlngs ...float64) (BatchResponse, error) { - res := BatchResponse{} + resp := BatchResponse{} if len(latlngs) == 0 { - return res, ErrReverseBatchMissingCoords + return resp, ErrReverseBatchMissingCoords } if len(latlngs)%2 == 1 { - return res, ErrReverseBatchInvalidCoordsPairs + return resp, ErrReverseBatchInvalidCoordsPairs } var ( @@ -74,15 +74,15 @@ func (g *Geocodio) ReverseBatch(latlngs ...float64) (BatchResponse, error) { pair = coord } - err := g.do("POST", "/reverse", nil, payload, &res) + err := g.do("POST", "/reverse", nil, payload, &resp) if err != nil { - return res, err + return resp, err } - if len(res.Results) == 0 { - return res, ErrNoResultsFound + if len(resp.Results) == 0 { + return resp, ErrNoResultsFound } - return res, nil + return resp, nil }