Pagination Error : Duplicate argument being outputted

This is another example of what is by far the most common mistake I’ve seen in WordPress development. I don’t think it’s even close to whatever the next most common is. I’m very curious as to what documentation or tutorials are out there that are teaching this approach to queries, or what could be done to the official documentation to make this clearer.

The problem is essentially that you’re using a pagination function, understrap_pagination() which is (correctly) using the status of the main query (the global $wp_query object) to determine the pages to show, but your template is using your own custom query ($search) to output the posts. If these queries have mismatched arguments, a different number posts per page, or a differing current page number, then you’re going to get weirdness like this.

All the main WordPress templates, like index.php, archive.php, search.php, have already queried the correct posts for you, based on the URL request, and made them accessible via the main loop:

if ( have_posts() ) : while( have_posts() ) : the_post();

endwhile; endif;

(Notice the lack of any object variable being used here)

And functions like pagination and next/previous links are all built to use this query to produce accurate output. If you use a separate query or modify this query incorrectly then those functions that depend on the state of the main query are going to give what looks like incorrect output. Pagination is the most common of these.

Your search.php loop needs to look like this:

$i = 1; 

if ( have_posts() ) : 
    while ( have_posts() ) : 
        the_post();

        get_template_part( 'loop-templates/content-catalogue', get_post_format() ); 

        if( $i % 3 == 0 ) {
            echo '<!-- Open a new deck --></div><div class="card-deck d-block d-lg-flex card-lg-gutters-3" data-aos="fade-up" data-aos-delay="50"><!-- Open a new deck -->'; 
        }

        $i++;

    endwhile;
else : 
    get_template_part( 'loop-templates/content', 'none' );
endif;

understrap_pagination();
wp_reset_postdata();

Note that $search isn’t used anywhere. This is because you must use the main query loop.

This brings us to the issue of why $search exists to begin with. From what I can tell from your full search.php template, you have 2 search forms: a “global” search, and a “catalogue” search, and you’re using the $search query when the user performs a catalogue search. Creating a separate query is not the correct way to do this. For one thing, it means that regardless of which search the user performed you’re performing both searches, which is ineffecient. Secondly, it’s causing you this pagination issue.

So what you need to do is make both the catalogue and global versions of your template use the same main loop, as I described above. Then you need to use the pre_get_posts hook to modify the main query based on which query the user performed. From what I can tell the main thing you do is change the number of posts per page, and limit the post type to products. You can do this with this code in your theme’s functions.php file:

function wpse_341991_catalogue_search( $query ) {
    if ( $query->is_search() ) {
        if ( 'catalogueSearch' === $_GET['formName'] ) {
            $query->set( 'post_type', 'products' );
            $query->set( 'posts_per_page', 15 );
        }
    }
}
add_action( 'pre_get_posts', 'wpse_341991_catalogue_search' );

Now whenever a user searches with your catalogue search form, the main query will show you the results you want, and the pagination functions should function like normal.

One last thing though. post_type is already a valid query argument that you can include in a search form. So if you want to change the post type searched by a form, you can just add a hidden field with the name set to post_type and the value set to the post type that you’re searching:

<input type="hidden" name="post_type" value="products">

This means you omit your formName input, and reduce the pre_get_posts function to:

function wpse_341991_catalogue_search( $query ) {
    if ( $query->is_search() && 'products' === $query->get( 'post_type' ) ) {
        $query->set( 'posts_per_page', 15 );
    }
}
add_action( 'pre_get_posts', 'wpse_341991_catalogue_search' );

And in your template, rather than checking:

if ( strcmp( $formIdentifier, 'globalSearch' )) {

You can check:

if ( 'products' !== get_query_var( 'post_type' ) ) {