예제 #1
0
def _clean_address(self):
    from common.geocode import geocode

    if "address" in self.initial and self.cleaned_data["address"] == self.initial["address"]:
        return self.initial["address"]

    result = None
    geocode_str = u"%s %s" % (self.cleaned_data["city"], self.cleaned_data["address"])
    geocode_results = geocode(geocode_str)
    if len(geocode_results) < 1:
        raise ValidationError("Could not resolve address")
    elif len(geocode_results) > 1:
        address_options = []
        for res in geocode_results:
            address = "%s %s" % (res["street_address"], res["house_number"])
            address_options.append(address)
            if address == self.cleaned_data["address"]:
                result = res
                break

        if not result:
            raise ValidationError("Please choose one: %s" % ", ".join(address_options))

    else:
        result = geocode_results[0]

    self.instance.lon = result["lon"]
    self.instance.lat = result["lat"]
    self.instance.geohash = result["geohash"]
    #        self.instance.save()

    self.cleaned_data["address"] = "%s %s" % (result["street_address"], result["house_number"])

    return self.cleaned_data["address"]
예제 #2
0
파일: util.py 프로젝트: WAYbetter/waybetter
def transliterate_english_order_to_hebrew(order, address_type):
    """
    Translate English order to Hebrew by transliterating the street name and geocoding the result, looking for a match.
    If no match is found, the transliterated result is returned.
    """

    street_address = getattr(order, "%s_street_address" % address_type)
    house_number = getattr(
        order, "%s_house_number" % address_type
    )  # or int(re.search("\d+", getattr(order, "%s_raw" % address_type)).group(0))
    city = getattr(order, "%s_city" % address_type)
    lat = getattr(order, "%s_lat" % address_type)
    lon = getattr(order, "%s_lon" % address_type)

    # transliterate English street name to Hebrew
    logging.info("queying google transliteration for %s" % street_address)
    hebrew_transliterator = Transliteration('iw')
    hebrew_street_address = hebrew_transliterator.getTransliteration(
        street_address.replace("'", ""))
    logging.info("transliteration returned %s" % hebrew_street_address)

    hebrew_address = hebrew_street_address
    if house_number:
        hebrew_address = u"%s %d" % (hebrew_street_address, house_number)

    # geocode transliterated address, and look for one with matching lon, lat
    results = geocode(hebrew_address, constrain_to_city=city)

    for result in results:
        if float(result["lat"]) == lat and float(result["lon"]) == lon:
            logging.info("telmap found a match")
            return u"%s %s, %s" % (result["street_address"],
                                   result["house_number"], result["city"])

    # try again without constrain to city
    hebrew_address = u"%s, %s" % (hebrew_address, city.name if city else "")
    results = geocode(hebrew_address)

    for result in results:
        if float(result["lat"]) == lat and float(result["lon"]) == lon:
            logging.info("telmap found a match")
            return u"%s %s, %s" % (result["street_address"],
                                   result["house_number"], result["city"])

    logging.info("telmap DID NOT find a match")
    return order.from_raw
예제 #3
0
def resolve_structured_address(request):
    city = get_object_or_404(City, id=request.GET.get("city_id"))
    street = request.GET.get("street", "")
    house_number = request.GET.get("house_number", "")

    address = "%s %s, %s" % (street.strip(), house_number.strip(), city.name)
    geocoding_results = geocode(address, resolve_to_ids=True)

    errors = None
    if not geocoding_results:
        # try replacing house number
        address = "%s %s, %s" % (street.strip(), 1, city.name)
        geocoding_results = geocode(address, resolve_to_ids=True)
        if geocoding_results:
            errors = {"house_number": _("Not found")}
        else:
            errors = {"street": _("Not found"), "house_number": _("Not found")}

    return JSONResponse({'geocoding_results': geocoding_results, 'errors': errors})
예제 #4
0
def test_well_formed_request(requests_mock):
    settings.GEOCODIO_KEY = "foobar"

    APIClient()
    geocodio_call = requests_mock.register_uri(
        "GET",
        API_ENDPOINT,
        json={
            "input": {
                "address_components": {"zip": "90024", "country": "US"},
                "formatted_address": "90024",
            },
            "results": [
                {
                    "address_components": {
                        "city": "Los Angeles",
                        "county": "Los Angeles County",
                        "state": "CA",
                        "zip": "90024",
                        "country": "US",
                    },
                    "formatted_address": "Los Angeles, CA 90024",
                    "location": {"lat": 34.065729, "lng": -118.434999},
                    "accuracy": 1,
                    "accuracy_type": "place",
                    "source": "TIGER\/Line\u00ae dataset from the US Census Bureau",
                }
            ],
        },
    )

    r = geocode(q="90024")

    assert geocodio_call.called
    assert geocodio_call.last_request.qs == {
        "api_key": [settings.GEOCODIO_KEY],
        "q": ["90024"],
    }
    assert r == [
        {
            "address_components": {
                "city": "Los Angeles",
                "county": "Los Angeles County",
                "state": "CA",
                "zip": "90024",
                "country": "US",
            },
            "formatted_address": "Los Angeles, CA 90024",
            "location": {"lat": 34.065729, "lng": -118.434999},
            "accuracy": 1,
            "accuracy_type": "place",
            "source": "TIGER\/Line\u00ae dataset from the US Census Bureau",
        }
    ]
예제 #5
0
def resolve_address(request, station):
    try:
        city = City.objects.filter(id=request.GET.get('city_id', '-1')).get()
    except City.DoesNotExist:
        return HttpResponseBadRequest(_("Invalid city"))

    address = request.GET.get('address', None)
    if not address:
        return JSONResponse('')

    return JSONResponse(geocode(address, constrain_to_city=city))
예제 #6
0
def resolve_address(request):
    # get parameters
    if not ADDRESS_PARAMETER in request.GET:
        return HttpResponseBadRequest("Missing address")

    address = request.GET[ADDRESS_PARAMETER]
    lon = request.GET.get("lon", None)
    lat = request.GET.get("lat", None)
    include_order_history = request.GET.get("include_order_history", True)
    fixed_address = fix_address(address, lon, lat)

    size = request.GET.get(MAX_SIZE_PARAMETER) or DEFAULT_RESULT_MAX_SIZE
    try:
        size = int(size)
    except:
        return HttpResponseBadRequest("Invalid value for max_size")

    geocoding_results = geocode(fixed_address,
                                max_size=size,
                                resolve_to_ids=True)
    history_results = []
    if include_order_history:
        passenger = Passenger.from_request(request)
        if passenger:
            history_results.extend([
                get_results_from_order(o, "from")
                for o in passenger.orders.filter(from_raw__icontains=address)
            ])
            history_results.extend([
                get_results_from_order(o, "to")
                for o in passenger.orders.filter(to_raw__icontains=address)
            ])
            history_results_by_name = {}
            for result in history_results:
                history_results_by_name[result["name"]] = result

            history_results = history_results_by_name.values()

            # remove duplicate results
            history_results_names = [
                result_name for result_name in history_results_by_name
            ]

            for result in geocoding_results:
                if result['name'] in history_results_names:
                    geocoding_results.remove(result)

    return JSONResponse({
        "geocode": geocoding_results[:size],
        "history": history_results[:size],
        "geocode_label": "map_suggestion",
        "history_label": "history_suggestion"
    })
예제 #7
0
def resolve_structured_address(request):
    city = get_object_or_404(City, id=request.GET.get("city_id"))
    street = request.GET.get("street", "")
    house_number = request.GET.get("house_number", "")

    address = "%s %s, %s" % (street.strip(), house_number.strip(), city.name)
    geocoding_results = geocode(address, resolve_to_ids=True)

    errors = None
    if not geocoding_results:
        # try replacing house number
        address = "%s %s, %s" % (street.strip(), 1, city.name)
        geocoding_results = geocode(address, resolve_to_ids=True)
        if geocoding_results:
            errors = {"house_number": _("Not found")}
        else:
            errors = {"street": _("Not found"), "house_number": _("Not found")}

    return JSONResponse({
        'geocoding_results': geocoding_results,
        'errors': errors
    })
예제 #8
0
def resolve_address(request):
    # get parameters
    if not ADDRESS_PARAMETER in request.GET:
        return HttpResponseBadRequest("Missing address")

    address = request.GET[ADDRESS_PARAMETER]
    lon = request.GET.get("lon", None)
    lat = request.GET.get("lat", None)
    include_order_history = request.GET.get("include_order_history", True)
    fixed_address = fix_address(address, lon, lat)

    size = request.GET.get(MAX_SIZE_PARAMETER) or DEFAULT_RESULT_MAX_SIZE
    try:
        size = int(size)
    except:
        return HttpResponseBadRequest("Invalid value for max_size")

    geocoding_results = geocode(fixed_address, max_size=size, resolve_to_ids=True)
    history_results = []
    if include_order_history:
        passenger = Passenger.from_request(request)
        if passenger:
            history_results.extend(
                [get_results_from_order(o, "from") for o in passenger.orders.filter(from_raw__icontains=address)]
            )
            history_results.extend(
                [get_results_from_order(o, "to") for o in passenger.orders.filter(to_raw__icontains=address)]
            )
            history_results_by_name = {}
            for result in history_results:
                history_results_by_name[result["name"]] = result

            history_results = history_results_by_name.values()

            # remove duplicate results
            history_results_names = [result_name for result_name in history_results_by_name]

            for result in geocoding_results:
                if result["name"] in history_results_names:
                    geocoding_results.remove(result)

    return JSONResponse(
        {
            "geocode": geocoding_results[:size],
            "history": history_results[:size],
            "geocode_label": "map_suggestion",
            "history_label": "history_suggestion",
        }
    )
예제 #9
0
    def resolve_lat_lon_or_default(city, street_address, house_number):
        lat = -1
        lon = -1
        street_address = street_address.strip()
        house_number = house_number.strip()
        address = "%s %s, %s" % (street_address, house_number, city.name)
        geocoding_results = geocode(address, resolve_to_ids=True)
        for result in geocoding_results:
            # make sure it is a match and not a suggestion
            if result["street_address"] == street_address and result["house_number"] == house_number:
                lat = result["lat"]
                lon = result["lon"]
                break

        return lat, lon
예제 #10
0
    def resolve_lat_lon_or_default(city, street_address, house_number):
        lat = -1
        lon = -1
        street_address = street_address.strip()
        house_number = house_number.strip()
        address = "%s %s, %s" % (street_address, house_number, city.name)
        geocoding_results = geocode(address, resolve_to_ids=True)
        for result in geocoding_results:
            # make sure it is a match and not a suggestion
            if result["street_address"] == street_address and result[
                    "house_number"] == house_number:
                lat = result["lat"]
                lon = result["lon"]
                break

        return lat, lon
예제 #11
0
def lookup_ga(
    item: Union[BallotRequest, Registration, Lookup, ReminderRequest]
) -> Tuple[str, Dict[str, str]]:
    from ovrlib import ga
    from common.geocode import geocode

    # geocode to a county
    addrs = geocode(
        street=item.address1,
        city=item.city,
        state="GA",
        zipcode=item.zipcode,
    )
    if not addrs:
        logger.warning(
            f"Unable to geocode {item} ({item.address1}, {item.city}, GA {item.zipcode})"
        )
        return None, None

    county = None
    for addr in addrs:
        if addr.get("address_components", {}).get("state") != "GA":
            continue
        county = addr.get("address_components", {}).get("county", "").upper()
        if county.endswith(" COUNTY"):
            county = county[0:-7]
        if county:
            break
    if not county:
        logger.warning(f"Unable to geocode county for {item}: addrs {addrs}")
        return None, None

    proxy, proxy_str = get_random_proxy_str_pair()
    logger.debug(f"lookup up GA {item} with proxy_str {proxy_str}")
    voter = ga.lookup_voter(
        first_name=item.first_name,
        last_name=item.last_name,
        date_of_birth=item.date_of_birth,
        county=county,
        proxies={"https": proxy_str},
    )
    if not voter:
        return None, None
    logger.info(voter)
    return voter.voter_reg_number, voter
예제 #12
0
def geocode_to_regions(street, city, state, zipcode):

    addrs = geocode(
        street=street,
        city=city,
        state=state,
        zipcode=zipcode,
        fields="stateleg",
    )
    if not addrs:
        logger.warning(
            f"address: Unable to geocode ({street}, {city}, {state} {zipcode})"
        )
        statsd.increment("turnout.official.address.failed_geocode")
        return None

    # use the first/best match
    addr = addrs[0]

    county = addr["address_components"].get("county")
    if not county:
        return None
    city = addr["address_components"].get("city")
    location = Point(addr["location"]["lng"], addr["location"]["lat"])

    state_code = addr["address_components"].get("state")
    if state_code != state:
        # if the address match gets the state wrong, return *no* result
        logger.warning(
            f"User-provided state {state} does not match geocoded state in {addr['address_components']}"
        )
        return None

    return match_region(
        city,
        county,
        state,
        location=location,
        districts=addr.get("fields", {}).get("state_legislative_districts",
                                             {}),
    )
예제 #13
0
def _clean_address(self):
    from common.geocode import geocode

    if "address" in self.initial and self.cleaned_data[
            "address"] == self.initial["address"]:
        return self.initial["address"]

    result = None
    geocode_str = u"%s %s" % (self.cleaned_data["city"],
                              self.cleaned_data["address"])
    geocode_results = geocode(geocode_str)
    if len(geocode_results) < 1:
        raise ValidationError("Could not resolve address")
    elif len(geocode_results) > 1:
        address_options = []
        for res in geocode_results:
            address = "%s %s" % (res["street_address"], res["house_number"])
            address_options.append(address)
            if address == self.cleaned_data["address"]:
                result = res
                break

        if not result:
            raise ValidationError("Please choose one: %s" %
                                  ", ".join(address_options))

    else:
        result = geocode_results[0]

    self.instance.lon = result["lon"]
    self.instance.lat = result["lat"]
    self.instance.geohash = result["geohash"]
    #        self.instance.save()

    self.cleaned_data["address"] = "%s %s" % (result["street_address"],
                                              result["house_number"])

    return self.cleaned_data["address"]
예제 #14
0
    def test_geo_coding(self):
        """
        Check that geocoding returns at least one location with correct geocode,
        i.e., country, city, street, house number, lon. and lat. are matching those of the query.
        """
        test_data = (
            {
                "address": u"דיזנגוף 99 תל אביב",
                "country": u"IL",
                "city": u"תל אביב יפו",
                "street_address": u"דיזנגוף",
                "house_number": u"99",
                "lon": '34.77388',
                "lat": '32.07933',
            },
            {
                "address": u"מרג 1 תל אביב יפו",
                "country": u"IL",
                "city": u"תל אביב יפו",
                "street_address": u"מרגולין",
                "house_number": u"1",
                "lon": '34.787368',
                "lat": '32.05856',
            },
            {
                "address": u"בן יהודה 35 ירושלים",
                "country": u"IL",
                "city": u"ירושלים",
                "street_address": u"אליעזר וחמדה בן יהודה",
                "house_number": u"35",
                "lon": '35.214161',
                "lat": '31.780725',
            },
        )

        logging.info("\nTesting geo coding")
        for test_case in test_data:
            test_case_success = False

            address = test_case["address"]
            logging.info("Testing geo coding for %s" % address)
            geo_code = geocode(address)
            self.assertTrue(geo_code,
                            msg="no geo code received for %s" % address)

            # geo_code may contain more than one location. Check that at least one is correct.
            for location in geo_code:
                location_success = True
                logging.info("Processing location %s" %
                             location["description"])

                # textual properties, compare lowercase
                for property in [
                        "country", "city", "street_address", "house_number"
                ]:
                    result = "OK"
                    if not test_case[property].lower(
                    ) == location[property].lower():
                        result = "failed"
                        location_success = False
                    #uncomment for debug since all Django tests run with DEBUG=False
                    #logging.info("comparing %s: %s" % (property, result))

                # numerical properties, allowed to differ a bit.
                precision = 0.001
                result = "OK"
                for property in ["lon", "lat"]:
                    if not abs(
                            float(test_case[property]) -
                            float(location[property])) < precision:
                        result = "failed"
                        location_success = False
                    #logging.info("comparing %s with precision %f: %s" % (property, precision, result))

                if location_success:
                    logging.info("Found correct location at %s" %
                                 location["description"])
                    test_case_success = True
                    break

            self.assertTrue(test_case_success,
                            msg="correct geo code was not found for %s" %
                            address)
예제 #15
0
파일: usvf.py 프로젝트: luisdomin5/turnout
def scrape_offices(session: requests.Session, regions: Sequence[Region]) -> None:
    existing_region_ids = [region.external_id for region in regions]

    existing_offices = Office.objects.values_list("external_id", flat=True)
    offices_dict: Dict[(int, Tuple[Action, Office])] = {}

    existing_addresses = {a.external_id: a for a in Address.objects.all()}
    addresses_dict: Dict[(int, Tuple[Action, Address])] = {}

    # The USVF API pagination is buggy; make multiple passes with
    # different page sizes.
    for limit in [100, 73, 67]:
        next_url = f"{API_ENDPOINT}/offices?limit={limit}"
        while next_url:
            with statsd.timed("turnout.official.usvfcall.offices", sample_rate=0.2):
                result = acquire_data(session, next_url)

            for office in result["objects"]:
                # Check that the region is valid (we don't support US territories)
                region_id = int(office["region"].rsplit("/", 1)[1])
                if region_id not in existing_region_ids:
                    continue

                office_id = office["id"]
                if office_id in offices_dict:
                    continue

                # Process each office in the response
                if office_id in existing_offices:
                    office_action = Action.UPDATE
                else:
                    office_action = Action.INSERT
                offices_dict[office_id] = (
                    office_action,
                    Office(
                        external_id=office_id,
                        region_id=int(office["region"].split("/")[-1]),
                        hours=office.get("hours"),
                        notes=office.get("notes"),
                    ),
                )

                for address in office.get("addresses", []):
                    # Process each address in the office
                    existing = existing_addresses.get(address["id"], None)
                    if existing:
                        address_action = Action.UPDATE
                        location = existing.location
                    else:
                        address_action = Action.INSERT
                        location = None
                    if not location and settings.USVF_GEOCODE:
                        addrs = geocode(
                            street=address.get("street1"),
                            city=address.get("city"),
                            state=address.get("state"),
                            zipcode=address.get("zip"),
                        )
                        if addrs:
                            location = Point(
                                addrs[0]["location"]["lng"], addrs[0]["location"]["lat"]
                            )
                    addresses_dict[address["id"]] = (
                        address_action,
                        Address(
                            external_id=address["id"],
                            office_id=office["id"],
                            address=address.get("address_to"),
                            address2=address.get("street1"),
                            address3=address.get("street2"),
                            city=address.get("city"),
                            state_id=address.get("state"),
                            zipcode=address.get("zip"),
                            website=address.get("website"),
                            email=address.get("main_email"),
                            phone=address.get("main_phone_number"),
                            fax=address.get("main_fax_number"),
                            location=location,
                            is_physical=address.get("is_physical"),
                            is_regular_mail=address.get("is_regular_mail"),
                            process_domestic_registrations="DOM_VR"
                            in address["functions"],
                            process_absentee_requests="DOM_REQ" in address["functions"],
                            process_absentee_ballots="DOM_RET" in address["functions"],
                            process_overseas_requests="OVS_REQ" in address["functions"],
                            process_overseas_ballots="OVS_RET" in address["functions"],
                        ),
                    )

            next_url = result["meta"].get("next")
        logger.info(
            "Found %(number)s offices after this pass", {"number": len(offices_dict)}
        )

    statsd.gauge("turnout.official.scraper.offices", len(offices_dict))
    logger.info("Found %(number)s Offices", {"number": len(offices_dict)})
    statsd.gauge("turnout.official.scraper.addresses", len(addresses_dict))
    logger.info("Found %(number)s Addresses", {"number": len(addresses_dict)})

    # Remove any records in our database but not in the result
    Office.objects.exclude(external_id__in=offices_dict.keys()).delete()
    Address.objects.exclude(external_id__in=addresses_dict.keys()).delete()

    # Create any records that are not already in our database
    Office.objects.bulk_create(
        [x[1] for x in offices_dict.values() if x[0] == Action.INSERT]
    )
    Address.objects.bulk_create(
        [x[1] for x in addresses_dict.values() if x[0] == Action.INSERT]
    )

    # Update any records that are already in our database
    def slow_bulk_update(cls, pk, records, keys):
        # this is slower than django's bulk_update(), but it (1)
        # respects modified_at and (2) works on aurora
        for r in records:
            obj = cls.objects.get(pk=r.pk)
            changed = False
            for k in keys:
                if getattr(obj, k) != getattr(r, k):
                    setattr(obj, k, getattr(r, k))
                    changed = True
            if changed:
                logger.info(f"Updated {obj}")
                obj.save()

    slow_bulk_update(
        Office,
        "external_id",
        [x[1] for x in offices_dict.values() if x[0] == Action.UPDATE],
        ["hours", "notes"],
    )
    slow_bulk_update(
        Address,
        "external_id",
        [x[1] for x in addresses_dict.values() if x[0] == Action.UPDATE],
        [
            "address",
            "address2",
            "address3",
            "city",
            "state",
            "zipcode",
            "website",
            "email",
            "phone",
            "fax",
            "is_physical",
            "is_regular_mail",
            "location",
            "process_domestic_registrations",
            "process_absentee_requests",
            "process_absentee_ballots",
            "process_overseas_requests",
            "process_overseas_ballots",
        ],
    )
예제 #16
0
    def test_geo_coding(self):
        """
        Check that geocoding returns at least one location with correct geocode,
        i.e., country, city, street, house number, lon. and lat. are matching those of the query.
        """
        test_data = (
            {"address"          : u"דיזנגוף 99 תל אביב",
             "country"          : u"IL",
             "city"             : u"תל אביב יפו",
             "street_address"   : u"דיזנגוף",
             "house_number"     : u"99",
             "lon"              : '34.77388',
             "lat"              : '32.07933',
        },
            {"address"          : u"מרג 1 תל אביב יפו",
             "country"          : u"IL",
             "city"             : u"תל אביב יפו",
             "street_address"   : u"מרגולין",
             "house_number"     : u"1",
             "lon"              : '34.787368',
             "lat"              : '32.05856',
        },
            {"address"          : u"בן יהודה 35 ירושלים",
             "country"          : u"IL",
             "city"             : u"ירושלים",
             "street_address"   : u"אליעזר וחמדה בן יהודה",
             "house_number"     : u"35",
             "lon"              : '35.214161',
             "lat"              : '31.780725',
            },
        )
        
        logging.info("\nTesting geo coding")
        for test_case in test_data:
            test_case_success = False

            address = test_case["address"]
            logging.info("Testing geo coding for %s" % address)
            geo_code = geocode(address)
            self.assertTrue(geo_code, msg="no geo code received for %s" % address)

            # geo_code may contain more than one location. Check that at least one is correct.
            for location in geo_code:
                location_success = True
                logging.info("Processing location %s" % location["description"])

                # textual properties, compare lowercase
                for property in ["country", "city", "street_address", "house_number"]:
                    result = "OK"
                    if not test_case[property].lower() == location[property].lower():
                        result = "failed"
                        location_success = False
                    #uncomment for debug since all Django tests run with DEBUG=False
                    #logging.info("comparing %s: %s" % (property, result))

                # numerical properties, allowed to differ a bit.
                precision = 0.001
                result = "OK"
                for property in ["lon", "lat"]:
                    if not abs(float(test_case[property]) - float(location[property])) < precision:
                        result = "failed"
                        location_success = False
                    #logging.info("comparing %s with precision %f: %s" % (property, precision, result))

                if location_success:
                    logging.info("Found correct location at %s" % location["description"])
                    test_case_success = True
                    break

            self.assertTrue(test_case_success, msg="correct geo code was not found for %s" % address)
예제 #17
0
def send_map_mms(
    number: Number,
    map_type: str,  # 'pp' or 'ev'
    address_full: str,
    content: str = None,
    blast: Blast = None,
) -> Optional[str]:
    formdata: Dict[str, str] = {}

    # geocode home
    home = geocode(q=address_full)
    home_address = address_full
    if not home:
        logger.info(f"{number}: Failed to geocode {address_full}")
        return f"Failed to geocode {address_full}"
    formdata["home_address_short"] = address_full.split(",")[0].upper()

    home_loc = f"{home[0]['location']['lng']},{home[0]['location']['lat']}"

    # geocode destination
    if get_feature_bool("locate_use_dnc_data",
                        home[0]["address_components"]["state"]):
        # pollproxy
        with tracer.trace("pollproxy.lookup", service="pollproxy"):
            response = requests.get(DNC_API_ENDPOINT,
                                    {"address": home_address})
        if response.status_code != 200:
            logger.warning(
                f"Got {response.status_code} from dnc query on {home_address}")
            return f"Failure querying data source"
        if map_type == "pp":
            dest = response.json().get("data",
                                       {}).get("election_day_locations", [])
            if not dest:
                logger.info(
                    f"{number}: No election day location for {address_full}: {response.json()}"
                )
                return f"No election day location for {address_full}"
            dest = dest[0]
        elif map_type == "ev":
            dest = response.json().get("data", {}).get("early_vote_locations",
                                                       [])
            if not dest:
                logger.info(
                    f"{number}: No early_vote location for {address_full}")
                return f"No early vote location for {address_full}"
            dest = dest[0]
        else:
            return f"Unrecognized address type {map_type}"

        dest_name = dest["location_name"]
        dest_address = (
            f"{dest['address_line_1']}, {dest['city']}, {dest['state']} {dest['zip']}"
        )
        dest_hours = dest["dates_hours"]
        if "lon" not in dest or "lat" not in dest:
            logger.warning(
                f"Lon/lat missing from {dest_address} (pollproxy): {dest}")
            return f"Lon/lat missing from {dest_address} (pollproxy)"
        dest_lon = dest["lon"]
        dest_lat = dest["lat"]
    else:
        # civic
        with tracer.trace("civicapi.voterlookup", service="google"):
            response = requests.get(
                CIVIC_API_ENDPOINT,
                {
                    "address": home_address,
                    "electionId": 7000,
                    "key": settings.CIVIC_KEY,
                },
            )
        if response.status_code != 200:
            logger.warning(
                f"Got {response.status_code} from civic query on {home_address}"
            )
            return f"Failure querying data source"
        if map_type == "pp":
            dest = response.json().get("pollingLocations", [])
            if not dest:
                logger.info(
                    f"{number}: No election day location for {address_full}: {dest}"
                )
                return f"No election day location for {address_full}"
            dest = dest[0]
        elif map_type == "ev":
            dest = response.json().get("earlyVoteSites", [])
            if not dest:
                logger.info(
                    f"{number}: No early_vote location for {address_full}")
                return f"No early vote location for {address_full}"
            dest = dest[0]
        else:
            return f"Unrecognized address type {map_type}"

        dest_name = dest["address"]["locationName"]
        dest_address = f"{dest['address']['line1']}, {dest['address']['city']}, {dest['address']['state']} {dest['address']['zip']}"
        dest_hours = dest["pollingHours"]
        if "longitude" not in dest or "latitude" not in dest:
            logger.warning(
                f"Lon/lat missing from {dest_address} (pollproxy): {dest}")
            return f"Lon/lat missing from {dest_address} (civic)"
        dest_lon = dest["longitude"]
        dest_lat = dest["latitude"]

    formdata["dest_name"] = dest_name.upper()
    formdata["dest_address"] = dest_address.upper()
    formdata["dest_hours"] = dest_hours

    # Pick a reasonable zoom level for the map, since mapbox pushes
    # the markers to the very edge of the map.
    #
    # mapbox zoom levels are 1-20, and go by power of 2: +1 zoom means
    # 1/4 of the map area.
    dx = abs(float(dest_lon) - float(home[0]["location"]["lng"]))
    dy = abs(float(dest_lat) - float(home[0]["location"]["lat"]))
    if dx > dy:
        d = dx
    else:
        d = dy
    logd = math.log2(1.0 / d)
    zoom = logd + 7.5

    if HIRES:
        size = "512x512"
    else:
        size = "256x256"
        zoom -= 1.0

    centerx = (float(dest_lon) + float(home[0]["location"]["lng"])) / 2
    centery = (float(dest_lat) + float(home[0]["location"]["lat"])) / 2
    map_loc = f"{centerx},{centery},{zoom}"

    # fetch map
    map_url = f"https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s-home+00f({home_loc}),pin-s-p+f00({dest_lon},{dest_lat})/{map_loc}/{size}?access_token={settings.MAPBOX_KEY}"
    response = requests.get(map_url)
    if response.status_code != 200:
        logger.warning(
            f"{number}: Failed to fetch map, got status code {response.status_code}"
        )
        return "Failed to fetch map"
    map_image = response.content
    map_image_type = response.headers["content-type"]

    # store map in s3
    filename = str(uuid.uuid4()) + "." + map_image_type.split("/")[-1]
    upload = s3_client.put_object(
        Bucket=settings.MMS_ATTACHMENT_BUCKET,
        Key=filename,
        ContentType=map_image_type,
        ACL="public-read",
        Body=map_image,
    )
    if upload.get("ResponseMetadata", {}).get("HTTPStatusCode") != 200:
        logger.warning(
            f"{number}: Unable to push {filename} to {settings.MMS_ATTACHMENT_BUCKET}"
        )
        return "Unable to upload map"
    stored_map_url = (
        f"https://{settings.MMS_ATTACHMENT_BUCKET}.s3.amazonaws.com/{filename}"
    )

    # locator link
    locator_url = f"https://www.voteamerica.com/where-to-vote/?{urlencode({'address':home_address})}"
    if blast:
        locator_url += f"&utm_medium=mms&utm_source=turnout&utm_campaign={blast.campaign}&source=va_mms_turnout_{blast.campaign}"
    formdata["locator_url"] = shorten_url(locator_url)

    # send
    number.send_sms(
        content.format(**formdata),
        media_url=[stored_map_url],
        blast=blast,
    )
    logger.info(
        f"Sent {map_type} map for {address_full} (blast {blast}) to {number}")
    return None