buildQuery and parseQuery URL helpers

TypeScript
export type QueryValue = string | number | boolean | null | undefined;

/** Encode flat values and arrays into a query string, skipping null/undefined. */
export function buildQuery(params: Record<string, QueryValue | QueryValue[]>, base = ''): string {
  const search = new URLSearchParams();
  for (const [key, value] of Object.entries(params)) {
    const values = Array.isArray(value) ? value : [value];
    for (const entry of values) {
      if (entry !== null && entry !== undefined) search.append(key, String(entry));
    }
  }
  const query = search.toString();
  if (!query || !base) return query || base;
  return `${base}${base.includes('?') ? '&' : '?'}${query}`;
}

/** Parse a full URL, a bare query or a hash fragment into URLSearchParams. */
export function parseQuery(input: string): URLSearchParams {
  const query = input.includes('?') ? input.slice(input.indexOf('?') + 1) : input.replace(/^#/, '');
  return new URLSearchParams(query.split('#')[0]);
}

/** Merge parsed params over defaults; explicit query values win. */
export function mergeQuery(params: URLSearchParams, defaults: Record<string, string>): Record<string, string> {
  const merged: Record<string, string> = { ...defaults };
  for (const [key, value] of params) merged[key] = value;
  return merged;
}