diff --git a/mu-plugins/blocks/google-map-event-filters/README.md b/mu-plugins/blocks/google-map-event-filters/README.md
new file mode 100644
index 000000000..197a3d026
--- /dev/null
+++ b/mu-plugins/blocks/google-map-event-filters/README.md
@@ -0,0 +1,33 @@
+# Google Map Event Filters
+
+This plugins creates the `wporg/google-map-event-filters` block, which displays a map and list of events that match a given filter during a given timeframe. Filters can be setup for anything, but some common examples are watch parties for WP anniversaries and the State of the Word.
+
+It uses the `wporg/google-map` block to display a searchable list and/or map of the selected events.
+
+
+## Usage
+
+1. Setup the API key needed for the `wporg/google-maps` block. See its README for details.
+1. Add a new filter to `filter_potential_events()` if you're not using an existing one.
+1. Add the following to a pattern in your theme. `googleMapBlockAttributes` are the attributes that will be passed to the `wporg/google-map` block, see it's README for details.
+
+ ```php
+ $filter_options = array(
+ 'filterSlug' => 'wp20',
+ 'startDate' => 'April 21, 2023',
+ 'endDate' => 'May 30, 2023',
+
+ // This has to be an object, see `WordPressdotorg\MU_Plugins\Google_Map_Event_Filters\render()`.
+ 'googleMapBlockAttributes' => (object) array(
+ 'id' => 'wp20',
+ 'apiKey' => 'WORDCAMP_DEV_GOOGLE_MAPS_API_KEY',
+ ),
+ );
+
+ ?>
+
+
+ ```
+
+1. View the page where the block is used. That will create the cron job that updates the data automatically in the future.
+1. Run `wp cron event run prime_event_filters` to test the filtering. Look at each title, and add any false positives to `$false_positives` in `filter_potential_events()`. If any events that should be included were ignored, add a keyword from the title to `$keywords`. Run the command after those changes and make sure it's correct now.
diff --git a/mu-plugins/blocks/google-map-event-filters/google-map-event-filters.php b/mu-plugins/blocks/google-map-event-filters/google-map-event-filters.php
new file mode 100644
index 000000000..16c96fe97
--- /dev/null
+++ b/mu-plugins/blocks/google-map-event-filters/google-map-event-filters.php
@@ -0,0 +1,229 @@
+ __NAMESPACE__ . '\render',
+ )
+ );
+}
+
+/**
+ * Render the block content.
+ */
+function render( array $attributes, string $content, WP_Block $block ): string {
+ $attributes['startDate'] = strtotime( $attributes['startDate'] );
+ $attributes['endDate'] = strtotime( $attributes['endDate'] );
+ $wrapper_attributes = get_block_wrapper_attributes( array( 'id' => 'wp-block-wporg-google-map-event-filters-' . $attributes['filterSlug'] ) );
+
+ // The REST API doesn't support associative arrays, so this had to be defined as an object in this block. It
+ // needs to be an array when passed to the Google Map block though.
+ // See `rest_is_array()`.
+ $map_options = (array) $attributes['googleMapBlockAttributes'];
+ $map_options['markers'] = get_events( $attributes['filterSlug'], $attributes['startDate'], $attributes['endDate'] );
+
+ // This has to be called in `render()` to know which slug/dates to use.
+ $cron_args = array( $attributes['filterSlug'], $attributes['startDate'], $attributes['endDate'], true );
+
+ if ( ! wp_next_scheduled( 'prime_event_filters', $cron_args ) ) {
+ wp_schedule_event(
+ time() + HOUR_IN_SECONDS,
+ 'hourly',
+ 'prime_event_filters',
+ $cron_args
+ );
+ }
+
+ ob_start();
+
+ ?>
+
+
>
+
+
+
+ prepare( '
+ SELECT
+ id, `type`, source_id, title, url, description, meetup, location, latitude, longitude, date_utc,
+ date_utc_offset AS tz_offset
+ FROM `wporg_events`
+ WHERE
+ status = "scheduled" AND
+ date_utc BETWEEN FROM_UNIXTIME( %d ) AND FROM_UNIXTIME( %d )
+ ORDER BY date_utc ASC
+ LIMIT 1000',
+ $start_timestamp,
+ $end_timestamp
+ );
+
+ if ( 'latin1' === DB_CHARSET ) {
+ $events = $wpdb->get_results( $query );
+ } else {
+ $events = get_latin1_results_with_prepared_query( $query );
+ }
+
+ foreach ( $events as $event ) {
+ // `capital_P_dangit()` won't work here because the current filter isn't `the_title` and there isn't a safelisted prefix before `$text`.
+ $event->title = str_replace( 'Wordpress', 'WordPress', $event->title );
+
+ // `date_utc` is a misnomer, the value is actually in the local timezone of the event. So, convert to a true Unix timestamp (UTC).
+ // Can't do this reliably in the query because MySQL converts it to the server timezone.
+ $event->timestamp = strtotime( $event->date_utc ) - $event->tz_offset;
+
+ unset( $event->date_utc );
+ }
+
+ return $events;
+}
+
+/**
+ * Query a table that's encoded with the `latin1` charset.
+ *
+ * wordpress.org uses a `DB_CHARSET` of `latin1` for legacy reasons, but wordcamp.org and others use `utf8mb4`.
+ * `wporg_events` uses `latin1`, so querying it with `utf8mb4` will produce Mojibake.
+ *
+ * @param string $prepared_query ⚠️ This must have already be ran through `$wpdb->prepare()` if needed.
+ *
+ * @return object|null
+ */
+function get_latin1_results_with_prepared_query( string $prepared_query ) {
+ global $wpdb;
+
+ // Local environments don't always use HyperDB, but production does.
+ $db_handle = is_a( $wpdb, 'hyperdb' ) ? $wpdb->db_connect( $prepared_query ) : $wpdb->dbh;
+ $wpdb->set_charset( $db_handle, 'latin1', 'latin1_swedish_ci' );
+
+ // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- This function doesn't have the context to prepare it, the caller must.
+ $results = $wpdb->get_results( $prepared_query );
+
+ // Revert to the default charset to avoid affecting other queries.
+ $wpdb->set_charset( $db_handle, DB_CHARSET, DB_COLLATE );
+
+ return $results;
+}
+
+/**
+ * Extract the desired events from an array of potential events.
+ */
+function filter_potential_events( string $filter_slug, array $potential_events ) : array {
+ $matched_events = array();
+ $other_events = array();
+
+ switch ( $filter_slug ) {
+ case 'sotw':
+ $false_positives = array();
+ $keywords = array(
+ 'sotw', 'state of the word',
+ );
+ break;
+
+ case 'wp20':
+ $false_positives = array( "292525625", "293437294" );
+ $keywords = array(
+ 'wp20', '20 year', '20 ano', '20 año', '20 candeline', '20 jaar', 'wordt 20', '20 yaşında',
+ 'anniversary', 'aniversário', 'aniversario', 'birthday', 'cumpleaños', 'celebrate',
+ 'Tanti auguri',
+ );
+ break;
+
+ default:
+ return array();
+ }
+
+ foreach ( $potential_events as $event ) {
+ $match = false;
+
+ // Have to use `source_id` because `id` is rotated by `REPLACE INTO` when table is updated.
+ if ( in_array( $event->source_id, $false_positives, true ) ) {
+ $other_events[] = $event;
+ continue;
+ }
+
+ foreach ( $keywords as $keyword ) {
+ if ( false !== stripos( $event->description, $keyword ) || false !== stripos( $event->title, $keyword ) ) {
+ // These are no longer needed, so remove it to save space in the database.
+ unset( $event->description );
+ unset( $event->source_id );
+ $matched_events[] = $event;
+ continue 2;
+ }
+ }
+
+ if ( ! $match ) {
+ $other_events[] = $event;
+ }
+ }
+
+ print_results( $matched_events, $other_events );
+
+ return $matched_events;
+}
+
+/**
+ * Print the matched/unmatched events for manual review.
+ *
+ * Run `wp cron event run prime_event_filters` to see this.
+ */
+function print_results( array $matched_events, array $other_events ) : void {
+ if ( 'cli' !== php_sapi_name() ) {
+ return;
+ }
+
+ $matched_names = wp_list_pluck( $matched_events, 'title' );
+ $other_names = wp_list_pluck( $other_events, 'title' );
+
+ sort( $matched_names );
+ sort( $other_names );
+
+ echo "\nIgnored these events. Double check for false-negatives.\n\n";
+ print_r( $other_names );
+
+ echo "\Included these events. Double check for false-positives.\n\n";
+ print_r( $matched_names );
+}
diff --git a/mu-plugins/blocks/google-map-event-filters/src/block.json b/mu-plugins/blocks/google-map-event-filters/src/block.json
new file mode 100644
index 000000000..e1572e524
--- /dev/null
+++ b/mu-plugins/blocks/google-map-event-filters/src/block.json
@@ -0,0 +1,33 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "wporg/google-map-event-filters",
+ "title": "Event Filters for the Google Map block",
+ "icon": "nametag",
+ "category": "widgets",
+ "description": "Displays a map and/or list of a subset of events.",
+ "textdomain": "wporg",
+ "attributes": {
+ "filterSlug": {
+ "type": "string",
+ "default": ""
+ },
+ "startDate": {
+ "type": "string",
+ "default": ""
+ },
+ "endDate": {
+ "type": "string",
+ "default": ""
+ },
+ "googleMapBlockAttributes": {
+ "type": "object",
+ "default": []
+ }
+ },
+ "supports": {
+ "inserter": false
+ },
+ "editorScript": "file:./index.js",
+ "style": "file:./style.css"
+}
diff --git a/mu-plugins/blocks/google-map-event-filters/src/index.js b/mu-plugins/blocks/google-map-event-filters/src/index.js
new file mode 100644
index 000000000..b5df1e45b
--- /dev/null
+++ b/mu-plugins/blocks/google-map-event-filters/src/index.js
@@ -0,0 +1,27 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { registerBlockType } from '@wordpress/blocks';
+import { Placeholder } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+
+function Edit() {
+ return (
+
+ );
+}
+
+registerBlockType( metadata.name, {
+ edit: Edit,
+} );
diff --git a/mu-plugins/loader.php b/mu-plugins/loader.php
index cf0cdaafb..2e061ab3a 100644
--- a/mu-plugins/loader.php
+++ b/mu-plugins/loader.php
@@ -34,6 +34,7 @@
require_once __DIR__ . '/blocks/local-navigation-bar/index.php';
require_once __DIR__ . '/blocks/latest-news/latest-news.php';
require_once __DIR__ . '/blocks/link-wrapper/index.php';
+require_once __DIR__ . '/blocks/google-map-event-filters/google-map-event-filters.php';
require_once __DIR__ . '/blocks/navigation/index.php';
require_once __DIR__ . '/blocks/notice/index.php';
require_once __DIR__ . '/blocks/query-filter/index.php';