Replace or extend the geofilter city list in code

Goal

Change what the geofilter location picker shows — and how it behaves — without touching the mam-geofilters core files. By the end you will know which of the four filters to reach for and how to wire each one up:

This is the code-level companion to the admin-side Recipe: Manage the city list and Recipe: Enable geofilters. Use the recipes when configuration is enough; use the filters here when it isn’t.


Prerequisites

  • mam-geofilters v2.1.1+ active, with the plugin entitlement granted (it self-exits in its constructor otherwise).
  • mam-main active.
  • A place to register filters — a small site-specific plugin or your theme’s functions.php. Do not edit files inside mam-geofilters.
  • Familiarity with WordPress add_filter().
  • For the “replace the list” path: a data source (a custom post type, an external API, or a taxonomy such as GeoDirectory).

One concept to hold onto before you start: there are two list paths, and only one runs per request.

The switch is a plain has_filter( 'mam_geofilter_list' ) check in includes/phone-manager.php. Pick the path that matches your goal; mixing them is the most common source of “my cities disappeared” bugs.


Steps

1. Decide: extend, or replace?

You want to… Use Path
Add a few markets to the built-in list mam_wc_geofilters_locations Default-list
Trim the built-in list to a regional subset mam_wc_geofilters_locations Default-list
Source cities from a CPT / API / taxonomy mam_geofilter_list Delegation

If you only need to add or remove a handful of cities, stay on the default-list path (Step 2). If your cities come from somewhere other than the hardcoded list, take over completely (Step 3).

2. Add or remove cities (mam_wc_geofilters_locations)

This filter receives the location array directly. Each entry has text, lat, and lon. You must return the array.

Add a city:

add_filter( 'mam_wc_geofilters_locations', 'my_app_add_rochester' );

function my_app_add_rochester( $locations ) {
    $locations[] = [
        'text' => 'Rochester, NY',
        'lat'  => '43.1566',
        'lon'  => '-77.6088',
    ];
    return $locations;
}

Trim to a regional subset:

add_filter( 'mam_wc_geofilters_locations', 'my_app_west_coast_only' );

function my_app_west_coast_only( $locations ) {
    $allowed = [ 'San Jose', 'Oakland', 'Los Angeles', 'San Diego', 'San Francisco' ];
    return array_values( array_filter(
        $locations,
        fn( $loc ) => in_array( $loc['text'], $allowed, true )
    ) );
}

The filter runs after the admin hide list is applied and before the alphabetical sort, the zero-coordinate drop, and the “Nearby” prepend. You don’t need to sort or prepend anything yourself.

Watch for:

  • Forgetting return $locations; strips the entire list.
  • lat or lon of 0 is silently dropped late in the builder. If your source defaults missing coordinates to 0, those entries vanish with no warning.
  • Duplicate text values confuse the admin show/hide UI, which keys on text.
  • This runs on every phone-data request. If you pull from a database or API, cache the result.

3. Replace the entire list (mam_geofilter_list)

This filter receives the whole mobile JSON payload ($data_array), not just the list. Your callback writes the array into $data_array['geoFiltersLocations'] and returns $data_array.

add_filter( 'mam_geofilter_list', 'my_app_geofilter_list' );

function my_app_geofilter_list( $data_array ) {
    $data_array['geoFiltersLocations'] = [
        [ 'text' => 'Brooklyn',  'lat' => '40.6782', 'lon' => '-73.9442', 'active' => 'on' ],
        [ 'text' => 'Manhattan', 'lat' => '40.7831', 'lon' => '-73.9712', 'active' => 'on' ],
        [ 'text' => 'Queens',    'lat' => '40.7282', 'lon' => '-73.7949', 'active' => 'on' ],
    ];
    return $data_array;
}

After your callback returns, the pipeline still drops entries whose active is set and not equal to 'on' (entries with no active key are kept), sorts alphabetically, and prepends a “Current Location” / GeoIP entry when appropriate. Let it do that work.

Watch for:

  • Any callback at any priority flips the switch. Even add_filter( 'mam_geofilter_list', '__return_array' ) makes has_filter() true and disables the default list. If you need to fall back to the default list conditionally, do it inside one callback — never remove_filter() from elsewhere.
  • mam_wc_geofilters_locations is bypassed on this path. Any additions you registered there will silently disappear once you delegate. This is exactly what happens when mam-geodirectory is active — it owns this filter and sources cities from GeoDirectory taxonomies.
  • Always return $data_array. Forgetting the return strips the whole payload, not just the list.

4. (Optional) Force geofilters on or off (mam_force_geofilters)

The on/off decision is resolved from the per-role settings tsl-use_geofilters and tsl-geofilters_show_as_icon, then passed through this filter as a bool for a last-word override. Return a bool.

add_filter( 'mam_force_geofilters', 'my_app_geofilters_for_premium' );

function my_app_geofilters_for_premium( $use_geofilters ) {
    if ( current_user_can( 'premium_member' ) ) {
        return true;
    }
    return $use_geofilters; // leave every other case untouched
}

Despite the name, this is a plain filter — returning false overrides a true upstream, so it can disable as well as enable. Useful for things the role-settings UI can’t express: a query parameter, a cookie, custom user meta, or a kill switch. If you toggle on $_REQUEST, sanitize it — this runs on every phone-data request.

5. (Optional) Override the search radius (mam_geofilter_radius)

The plugin’s own callback resolves the radius (in miles) in this order: $_REQUEST['radius'] → per-role setting tsl-setting-geofilter_radius → default 25 → capped at 3000. Register your callback at a later priority to override the resolved value. Return an int.

add_filter( 'mam_geofilter_radius', 'my_app_premium_radius', 20 );

function my_app_premium_radius( $radius ) {
    if ( current_user_can( 'premium_member' ) ) {
        return min( 100, max( $radius, 50 ) );
    }
    return $radius;
}

Watch for:

  • The 3000-mile cap only fires inside the core callback. A later callback can return any positive integer; re-apply min( 3000, $radius ) yourself if you widen it and still want the cap.
  • $_REQUEST['radius'] short-circuits the per-role setting. If a user’s radius doesn’t match their role config, check whether the client is sending an explicit radius parameter.
  • This filter does not turn the picker on or off — a radius of 0 doesn’t disable it. Use mam_force_geofilters for that.

6. Verify in the app

Trigger a phone-data request (refresh the app or hit the phone-data endpoint) and confirm:

  • Your added/removed cities appear (or your replacement list does).
  • The picker is present or absent as your mam_force_geofilters logic intends.
  • Distance filtering matches the radius you set.

Because the list builder consumes the admin hide option before mam_wc_geofilters_locations, cities you add in code only become visible to the admin show/hide UI after a request has populated the list at least once.


  • Hook: mam_wc_geofilters_locations — full reference for adding/removing cities
  • Hook: mam_geofilter_list — full reference for replacing the list
  • Hook: mam_force_geofilters — full reference for the on/off override
  • Hook: mam_geofilter_radius — full reference for the radius override
  • Recipe: Manage the city list — admin-side hide/edit, no code
  • Recipe: Enable geofilters — admin-side on/off and radius
  • Plugin overview: mam-geofilters

What’s next

If your replacement list comes from a real data source, wrap the lookup in a transient — the relevant filter fires on every phone-data request, and an uncached database or API call there will slow every app load. Once your code-level list is stable, hand off to the admin recipes so non-developers can hide individual cities without redeploying.

Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!